"> '); ","noIndex":false,"favicon":"","contentLanguage":"en","copyrightEnabled":false,"copyrightText":"","keywords":"","author":"timothyredelfotography.com","language":"en","locale":"en_US","siteName":"timothyredelfotography.com"}; console.log('Client Initialized site metadata:', window.siteMetadata); console.log('Client Google Analytics from metadataToUse:', ' '); console.log('Client Google Analytics in window.siteMetadata:', window.siteMetadata.googleAnalytics); console.log('Client Content Language:', window.siteMetadata.contentLanguage); // State Management let galleries = [{"id":1748576989763,"title":"GETTING STARTED","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"page_1748576989763","visible":false,"parentId":null,"siteId":"tma73fpp","slug":"getting-started","url":"/getting-started","pageElements":[{"id":1748576998046,"type":"metadata","visible":true,"position":1,"metaTitle":"","metaDescription":"","metaKeywords":""},{"id":1748577017097,"type":"text","title":"Text Block","visible":true,"position":1,"textContent":"


GETTING STARTED


","textWidth":100},{"id":1748577013784,"type":"embedded-content","title":"New Element","visible":true,"position":2,"contentUrl":"https://player.vimeo.com/video/1088272581?h=7405cc3400&badge=0&autopause=0&player_id=0&app_id=58479","containerWidth":82,"containerHeight":67,"useAutoHeight":false}]},{"id":1748577062628,"title":"Spacer","parentId":null,"visible":true,"isSpacer":true},{"id":1748282240591,"title":"Home","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"page_1748282240591","visible":false,"parentId":null,"siteId":"tma73fpp","slug":"home","url":"/home","pageElements":[{"id":1748282250301,"type":"metadata","title":"Metadata","visible":true,"position":0,"metaTitle":"Timothy Redel Photography","metaDescription":"Timothy Redel is an American photographer based in Hong Kong.","metaKeywords":""},{"id":1753734466803,"type":"text","title":"Text Block","visible":true,"position":1,"textContent":"

<!-- Google tag (gtag.js) -->

<script async src=\"https://www.googletagmanager.com/gtag/js?id=G-207MJXF5C4\"></script>

<script>

window.dataLayer = window.dataLayer || [];

function gtag(){dataLayer.push(arguments);}

gtag('js', new Date());


gtag('config', 'G-207MJXF5C4');

</script>


","textWidth":50},{"id":1748282254402,"type":"column-container","title":"Columns","visible":true,"position":2,"columns":[{"id":1748282257244,"hAlign":"left","vAlign":"top","elements":[{"id":1746063080696,"type":"image","title":"Image","visible":true,"position":0,"imageWidth":"100%","imageAlignment":"center","imageUrl":"https://storage.neonsky.app/tma73fpp/1748282275203_TimothyRedel_2011_07.jpg","imageKey":"tma73fpp/1748282275203_TimothyRedel_2011_07.jpg","imageFilename":"TimothyRedel_2011_07.jpg","imageType":"image/jpeg"}]},{"id":1748282251209,"hAlign":"left","vAlign":"top","elements":[{"id":1746063085138,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"


Timothy Redel | Hong Kong Photographer

Timothy Redel is an American photographer from NYC who lives in Hong Kong. 


","textWidth":84}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"}}],"isHomePage":true},{"id":1748285215729,"title":"Selected Work","parentId":null,"visible":true,"isFolder":true,"isCollapsed":true,"isHomePage":false},{"id":1748281880570,"title":"Black & White","url":"/black-white","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":1748285215729,"siteId":"tma73fpp","pageId":"9rnb09va","classicCategoryId":2986,"classicGuid":"4bd5ebf3a78e0","importSource":"classic-gallery","slug":"black-white","normalizedName":"black-white","galleryOptions":{"manualCollectionName":"GUID=tma73fpp","categoryId":2986,"importedFromClassic":true,"siteId":"tma73fpp","pageId":"9rnb09va","siteAlias":"preview.neonsky.app","initialPageUuid":"9rnb09va","initialPageAlias":"black-white","timestamp":1760048354241,"horizontalScrollerText":"


","startInSingles":true,"layoutType":"singles","columns":"3","spacing":1.5,"autoplaySingles":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":false,"showFilename":false,"displayAllInfo":false,"navigationMode":"dynamicCursor","showTextBlock":false,"fixedHeroImage":false,"zoomInLightbox":false,"lightboxOnMobile":false,"fadeDuration":"500","fadeInDuration":100,"descriptionTextColor":"0, 0, 0","gridImageOverlayColor":"130, 130, 130","gridImageOverlayOpacity":"0.5","lightboxBgColor":"255, 255, 255","lightboxBgOpacity":"1","lightboxCloseColor":"0, 0, 0","lightboxArrowColor":"0, 0, 0","useTitles":false,"useLinks":false,"desktopTitleDisplayMode":"overlay","titleTextAlign":"center","showDescriptionInOverlay":false,"includeRolloverImageInOverlay":true,"filterMenuEnabled":false,"filterMenuCollectionNames":"","filterMenuTitles":"","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionNames":"","openMultipleLightboxes":false,"batchSize":"20","jsonCollections":{"GUID=tma73fpp":{"isClassicCollection":true,"guid":"GUID=tma73fpp","metadata":{},"totalItems":0}},"descriptionsOnDemand":false,"thumbnailNavigation":false,"isPerma":true,"txId":"M1jo1J7HLGoeXrDYzluHtzTrVmip9xPJa7twADKtHRI","permaURL":"M1jo1J7HLGoeXrDYzluHtzTrVmip9xPJa7twADKtHRI","uploadedTo":["fly","irys"],"fileSize":45295},"isHomePage":false,"siteAlias":null},{"id":1748281880563,"title":"Color","url":"/color","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":1748285215729,"siteId":"tma73fpp","pageId":"n1t3jn8t","classicCategoryId":2635,"classicGuid":"4bd5ebf3a78e0","importSource":"classic-gallery","slug":"color","normalizedName":"color","galleryOptions":{"manualCollectionName":"GUID=tma73fpp","categoryId":2635,"importedFromClassic":true,"siteId":"tma73fpp","pageId":"n1t3jn8t","siteAlias":"preview.neonsky.app","initialPageUuid":"n1t3jn8t","initialPageAlias":"color","timestamp":1760048647815,"horizontalScrollerText":"


","startInSingles":true,"layoutType":"singles","columns":"3","spacing":1.6,"autoplaySingles":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":false,"showFilename":false,"displayAllInfo":false,"navigationMode":"dynamicCursor","showTextBlock":false,"fixedHeroImage":false,"zoomInLightbox":false,"lightboxOnMobile":false,"fadeDuration":"500","fadeInDuration":100,"descriptionTextColor":"0, 0, 0","gridImageOverlayColor":"130, 130, 130","gridImageOverlayOpacity":"0.5","lightboxBgColor":"255, 255, 255","lightboxBgOpacity":"1","lightboxCloseColor":"0, 0, 0","lightboxArrowColor":"0, 0, 0","useTitles":false,"useLinks":false,"desktopTitleDisplayMode":"overlay","titleTextAlign":"center","showDescriptionInOverlay":false,"includeRolloverImageInOverlay":true,"filterMenuEnabled":false,"filterMenuCollectionNames":"","filterMenuTitles":"","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionNames":"","openMultipleLightboxes":false,"batchSize":"20","jsonCollections":{"GUID=tma73fpp":{"isClassicCollection":true,"guid":"GUID=tma73fpp","metadata":{},"totalItems":0}},"descriptionsOnDemand":false,"thumbnailNavigation":false,"isPerma":true,"txId":"Df4zNHv1xYF3FR-QiVMT_cRB3ytAKYn0DsjqS16sZ2U","permaURL":"Df4zNHv1xYF3FR-QiVMT_cRB3ytAKYn0DsjqS16sZ2U","uploadedTo":["fly","irys"],"fileSize":17771},"isHomePage":false,"siteAlias":null},{"id":1748281880633,"title":"Aerial","url":"/aerial","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":1748285215729,"siteId":"tma73fpp","pageId":"u41e17r7","classicCategoryId":178468,"classicGuid":"4bd5ebf3a78e0","importSource":"classic-gallery","slug":"aerial","normalizedName":"aerial","galleryOptions":{"manualCollectionName":"GUID=tma73fpp","categoryId":178468,"importedFromClassic":true,"siteId":"tma73fpp","pageId":"u41e17r7","siteAlias":"preview.neonsky.app","initialPageUuid":"u41e17r7","initialPageAlias":"aerial","timestamp":1760048715820,"horizontalScrollerText":"


","startInSingles":true,"layoutType":"singles","columns":"3","spacing":1.7,"autoplaySingles":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":false,"showFilename":false,"displayAllInfo":false,"navigationMode":"dynamicCursor","showTextBlock":false,"fixedHeroImage":false,"zoomInLightbox":false,"lightboxOnMobile":false,"fadeDuration":"500","fadeInDuration":100,"descriptionTextColor":"0, 0, 0","gridImageOverlayColor":"130, 130, 130","gridImageOverlayOpacity":"0.5","lightboxBgColor":"255, 255, 255","lightboxBgOpacity":"1","lightboxCloseColor":"0, 0, 0","lightboxArrowColor":"0, 0, 0","useTitles":false,"useLinks":false,"desktopTitleDisplayMode":"overlay","titleTextAlign":"center","showDescriptionInOverlay":false,"includeRolloverImageInOverlay":true,"filterMenuEnabled":false,"filterMenuCollectionNames":"","filterMenuTitles":"","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionNames":"","openMultipleLightboxes":false,"batchSize":"20","jsonCollections":{"GUID=tma73fpp":{"isClassicCollection":true,"guid":"GUID=tma73fpp","metadata":{},"totalItems":0}},"descriptionsOnDemand":false,"thumbnailNavigation":false,"isPerma":true,"txId":"_H12UhByR-M9nQLjShDWMQmQ75rJrS5DERBYL5lKZ40","permaURL":"_H12UhByR-M9nQLjShDWMQmQ75rJrS5DERBYL5lKZ40","uploadedTo":["fly","irys"],"fileSize":11115},"isHomePage":false,"siteAlias":null},{"id":1748285261661,"title":"Travel","parentId":1748285215729,"visible":true,"isFolder":true,"isCollapsed":true,"isHomePage":false},{"id":1748281880619,"title":"Italy ","url":"/italy","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":1748285261661,"siteId":"tma73fpp","pageId":"4kcx7qxw","classicCategoryId":50363,"classicGuid":"4bd5ebf3a78e0","importSource":"classic-gallery","slug":"italy","normalizedName":"italy","galleryOptions":{"manualCollectionName":"GUID=tma73fpp","categoryId":50363,"importedFromClassic":true,"siteId":"tma73fpp","pageId":"4kcx7qxw","siteAlias":"preview.neonsky.app","initialPageUuid":"4kcx7qxw","initialPageAlias":"italy","timestamp":1748286671404,"horizontalScrollerText":"


","startInSingles":true,"layoutType":"singles","columns":"3","spacing":2,"autoplaySingles":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":false,"showFilename":false,"displayAllInfo":false,"navigationMode":"dynamicCursor","showTextBlock":false,"fixedHeroImage":false,"zoomInLightbox":false,"lightboxOnMobile":false,"fadeDuration":"500","fadeInDuration":100,"descriptionTextColor":"0, 0, 0","gridImageOverlayColor":"130, 130, 130","gridImageOverlayOpacity":"0.5","lightboxBgColor":"255, 255, 255","lightboxBgOpacity":"1","lightboxCloseColor":"0, 0, 0","lightboxArrowColor":"0, 0, 0","useTitles":false,"useLinks":false,"desktopTitleDisplayMode":"overlay","titleTextAlign":"center","showDescriptionInOverlay":false,"includeRolloverImageInOverlay":true,"filterMenuEnabled":false,"filterMenuCollectionNames":"","filterMenuTitles":"","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionNames":"","openMultipleLightboxes":false,"batchSize":"20","jsonCollections":{"GUID=tma73fpp":{"isClassicCollection":true,"guid":"GUID=tma73fpp","metadata":{},"totalItems":0}}},"isHomePage":false,"siteAlias":null},{"id":1748281880549,"title":"Hong Kong","url":"/timothy-redel-hong-kong-fotographer","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":1748285261661,"siteId":"tma73fpp","pageId":"idm79vff","classicCategoryId":2631,"classicGuid":"4bd5ebf3a78e0","importSource":"classic-gallery","slug":"timothy-redel-hong-kong-fotographer","normalizedName":"timothy-redel-hong-kong-fotographer","galleryOptions":{"manualCollectionName":"GUID=tma73fpp","categoryId":2631,"importedFromClassic":true,"siteId":"tma73fpp","pageId":"idm79vff","siteAlias":"preview.neonsky.app","initialPageUuid":"idm79vff","initialPageAlias":"timothy-redel-hong-kong-fotographer","timestamp":1748286700218,"horizontalScrollerText":"


","startInSingles":true,"layoutType":"singles","columns":"3","spacing":2,"autoplaySingles":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":false,"showFilename":false,"displayAllInfo":false,"navigationMode":"dynamicCursor","showTextBlock":false,"fixedHeroImage":false,"zoomInLightbox":false,"lightboxOnMobile":false,"fadeDuration":"500","fadeInDuration":100,"descriptionTextColor":"0, 0, 0","gridImageOverlayColor":"130, 130, 130","gridImageOverlayOpacity":"0.5","lightboxBgColor":"255, 255, 255","lightboxBgOpacity":"1","lightboxCloseColor":"0, 0, 0","lightboxArrowColor":"0, 0, 0","useTitles":false,"useLinks":false,"desktopTitleDisplayMode":"overlay","titleTextAlign":"center","showDescriptionInOverlay":false,"includeRolloverImageInOverlay":true,"filterMenuEnabled":false,"filterMenuCollectionNames":"","filterMenuTitles":"","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionNames":"","openMultipleLightboxes":false,"batchSize":"20","jsonCollections":{"GUID=tma73fpp":{"isClassicCollection":true,"guid":"GUID=tma73fpp","metadata":{},"totalItems":0}}},"metaTitle":"","metaDescription":"","hideMenuOnPage":false,"siteAlias":null,"isHomePage":false},{"id":1748281880640,"title":"Vietnam","url":"/vietnam","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":1748285261661,"siteId":"tma73fpp","pageId":"31k2don0","classicCategoryId":182590,"classicGuid":"4bd5ebf3a78e0","importSource":"classic-gallery","slug":"vietnam","normalizedName":"vietnam","galleryOptions":{"manualCollectionName":"GUID=tma73fpp","categoryId":182590,"importedFromClassic":true,"siteId":"tma73fpp","pageId":"31k2don0","siteAlias":"preview.neonsky.app","initialPageUuid":"31k2don0","initialPageAlias":"vietnam","timestamp":1748286759198,"horizontalScrollerText":"


","startInSingles":true,"layoutType":"singles","columns":"3","spacing":2,"autoplaySingles":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":false,"showFilename":false,"displayAllInfo":false,"navigationMode":"dynamicCursor","showTextBlock":false,"fixedHeroImage":false,"zoomInLightbox":false,"lightboxOnMobile":false,"fadeDuration":"500","fadeInDuration":100,"descriptionTextColor":"0, 0, 0","gridImageOverlayColor":"130, 130, 130","gridImageOverlayOpacity":"0.5","lightboxBgColor":"255, 255, 255","lightboxBgOpacity":"1","lightboxCloseColor":"0, 0, 0","lightboxArrowColor":"0, 0, 0","useTitles":false,"useLinks":false,"desktopTitleDisplayMode":"overlay","titleTextAlign":"center","showDescriptionInOverlay":false,"includeRolloverImageInOverlay":true,"filterMenuEnabled":false,"filterMenuCollectionNames":"","filterMenuTitles":"","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionNames":"","openMultipleLightboxes":false,"batchSize":"20","jsonCollections":{"GUID=tma73fpp":{"isClassicCollection":true,"guid":"GUID=tma73fpp","metadata":{},"totalItems":0}}},"isHomePage":false,"siteAlias":null},{"id":1748285961523,"title":"Info","parentId":null,"visible":true,"isFolder":true,"isCollapsed":true,"isHomePage":false},{"id":1748281880533,"title":"Clients","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"page_1748281880533","visible":true,"parentId":1748285961523,"siteId":"4bd5ebf3a78e0","slug":"clients","url":"/clients","pageElements":[{"id":1748281881533,"type":"metadata","title":"Metadata","visible":true,"position":0,"metaTitle":"American Express, BusinessWeek, Bloomberg, Cosmopolitan, Eaton, GQ, Forbes, Glamour, Harper Collins, IBM","metaDescription":"American Express, BusinessWeek, Bloomberg, Cosmopolitan, Eaton, GQ, Forbes, Glamour, Harper Collins, IBM","metaKeywords":""},{"id":1748281882533,"type":"text","title":"Text Block Spacer","visible":true,"position":1,"textContent":"

 


","textWidth":100},{"id":1748281883533,"type":"column-container","title":"Imported Columns","visible":true,"position":2,"columns":[{"id":1748281883833,"hAlign":"right","vAlign":"middle","elements":[{"id":1748281883633,"type":"slideshow","title":"Image Slideshow","visible":true,"position":0,"slides":[{"imageUrl":"https://storage.neonsky.app/4bd5ebf3a78e0/images/TimothyRedel_2011_07.jpg","imageKey":"4bd5ebf3a78e0/images/TimothyRedel_2011_07.jpg","imageFilename":"TimothyRedel_2011_07.jpg","imageType":"image/jpeg"}],"slideDuration":5000,"transitionDuration":500,"slideshowWidth":88,"slideshowHeight":50,"showFullImages":true}]},{"id":1748281883933,"hAlign":"left","vAlign":"middle","elements":[{"id":1748281883733,"type":"text","title":"Text Block Content","visible":true,"position":0,"textContent":"

PARTIAL LISTING


American Express

BusinessWeek

Bloomberg

Cosmopolitan

Eaton

Forbes

GQ

Glamour

Harper Collins

IBM

Mass Mutual Insurance

New York

People

Revolution

Rolling Stone

The Steve Case Foundation

SEIU

Space Adventures

TIME

Town & Country

The Washington Post

XM Satellite Radio


","textWidth":70}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false}}],"classicCategoryId":2921,"classicGuid":"4bd5ebf3a78e0","importSource":"classic-page","isHomePage":false},{"id":1748281880542,"title":"Capabilities","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"page_1748281880542","visible":true,"parentId":1748285961523,"siteId":"4bd5ebf3a78e0","slug":"capabilities","url":"/capabilities","pageElements":[{"id":1748281881542,"type":"metadata","title":"Metadata","visible":true,"position":0,"metaTitle":"Advertising, Private Commissions, Annual Report, Editorial, Fine Art, Stock, Photo Illustration.","metaDescription":"Advertising, Private Commissions, Annual Report, Editorial, Fine Art, Stock, Photo Illustration.","metaKeywords":""},{"id":1748281882542,"type":"text","title":"Text Block Spacer","visible":true,"position":1,"textContent":"

 


","textWidth":100},{"id":1748281883542,"type":"column-container","title":"Imported Columns","visible":true,"position":2,"columns":[{"id":1748281883842,"hAlign":"right","vAlign":"middle","elements":[{"id":1748281883642,"type":"slideshow","title":"Image Slideshow","visible":true,"position":0,"slides":[{"imageUrl":"https://storage.neonsky.app/4bd5ebf3a78e0/images/IceSkater.jpg","imageKey":"4bd5ebf3a78e0/images/IceSkater.jpg","imageFilename":"IceSkater.jpg","imageType":"image/jpeg"}],"slideDuration":5000,"transitionDuration":500,"slideshowWidth":88,"slideshowHeight":50,"showFullImages":true}]},{"id":1748281883942,"hAlign":"left","vAlign":"middle","elements":[{"id":1748281883742,"type":"text","title":"Text Block Content","visible":true,"position":0,"textContent":"

Powered by Leica, Hasselblad and DJI


Advertising Photography

Annual Report Photography

Editorial Photography

Fine Art

Stock Photography

Photo Illustration

Private Commissions







","textWidth":70}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false}}],"classicCategoryId":2922,"classicGuid":"4bd5ebf3a78e0","importSource":"classic-page","isHomePage":false},{"id":1748281880598,"title":"LinkedIn","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"page_1748281880598","visible":true,"parentId":1748285961523,"siteId":"4bd5ebf3a78e0","slug":"linkedin","url":"/linkedin","pageElements":[{"id":1748281881598,"type":"metadata","title":"Metadata","visible":true,"position":0,"metaTitle":"Professional business network with 1000+ contacts.","metaDescription":"Professional business network with 1000 + contacts.","metaKeywords":""},{"id":1748281882598,"type":"text","title":"Text Block Spacer","visible":true,"position":1,"textContent":"

 


","textWidth":100},{"id":1748281883598,"type":"column-container","title":"Imported Columns","visible":true,"position":2,"columns":[{"id":1748281883898,"hAlign":"right","vAlign":"middle","elements":[]},{"id":1748281883998,"hAlign":"left","vAlign":"middle","elements":[{"id":1748281883798,"type":"text","title":"Text Block Content","visible":true,"position":0,"textContent":"

View My Profile

","textWidth":70}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false}}],"classicCategoryId":31714,"classicGuid":"4bd5ebf3a78e0","importSource":"classic-page","isHomePage":false},{"id":1748281880584,"title":"Memberships","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"page_1748281880584","visible":true,"parentId":1748285961523,"siteId":"4bd5ebf3a78e0","slug":"memberships","url":"/memberships","pageElements":[{"id":1748281881584,"type":"metadata","title":"Metadata","visible":true,"position":0,"metaTitle":"APA - American Photographic Artists, EP - Editorial Photographers","metaDescription":"APA - American Photographic Artists, EP - Editorial Photographers","metaKeywords":""},{"id":1748281882584,"type":"text","title":"Text Block Spacer","visible":true,"position":1,"textContent":"

 


","textWidth":100},{"id":1748281883584,"type":"column-container","title":"Imported Columns","visible":true,"position":2,"columns":[{"id":1748281883884,"hAlign":"right","vAlign":"middle","elements":[{"id":1748281883684,"type":"slideshow","title":"Image Slideshow","visible":true,"position":0,"slides":[{"imageUrl":"https://storage.neonsky.app/4bd5ebf3a78e0/images/APA_logo.jpg","imageKey":"4bd5ebf3a78e0/images/APA_logo.jpg","imageFilename":"APA_logo.jpg","imageType":"image/jpeg"}],"slideDuration":5000,"transitionDuration":500,"slideshowWidth":88,"slideshowHeight":50,"showFullImages":true}]},{"id":1748281883984,"hAlign":"left","vAlign":"middle","elements":[{"id":1748281883784,"type":"text","title":"Text Block Content","visible":true,"position":0,"textContent":"


APA - American Photographic Artists


American Photographic Artists, the leading trade association representing the interests of advertising photographers, works to improve the environment for success in the industry and champions the rights of photographers worldwide.


EP - Editorial Photographers


Editorial Photographers is an organization of top magazine and news photographers from around the world dedicated to improving business practices and contracts.





","textWidth":70}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false}}],"classicCategoryId":7611,"classicGuid":"4bd5ebf3a78e0","importSource":"classic-page","isHomePage":false},{"id":1748281880605,"title":"About ","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"page_1748281880605","visible":true,"parentId":null,"siteId":"4bd5ebf3a78e0","slug":"about","url":"/about","pageElements":[{"id":1748281881605,"type":"metadata","title":"Metadata","visible":true,"position":0,"metaTitle":"Award-winning American photographer from NYC who lives in Hong Kong. ","metaDescription":"Award-winning American photographer from NYC who lives in Hong Kong. ","metaKeywords":""},{"id":1748281882605,"type":"text","title":"Text Block Spacer","visible":true,"position":1,"textContent":"

 


","textWidth":100},{"id":1748281883605,"type":"column-container","title":"Imported Columns","visible":true,"position":2,"columns":[{"id":1748281883905,"hAlign":"right","vAlign":"top","elements":[{"id":1748281883705,"type":"slideshow","title":"Image Slideshow","visible":true,"position":0,"slides":[{"imageUrl":"https://storage.neonsky.app/4bd5ebf3a78e0/images/TimothyRedel_portrait1.jpg","imageKey":"4bd5ebf3a78e0/images/TimothyRedel_portrait1.jpg","imageFilename":"TimothyRedel_portrait1.jpg","imageType":"image/jpeg"}],"slideDuration":5000,"transitionDuration":500,"slideshowWidth":88,"slideshowHeight":50,"showFullImages":true}]},{"id":1748281884005,"hAlign":"left","vAlign":"middle","elements":[{"id":1748281883805,"type":"text","title":"Text Block Content","visible":true,"position":0,"textContent":"

Timothy Redel is an American photographer from NYC who lives in Hong Kong.


Redel's portraits of artists, celebrities, business leaders and politicians have graced the covers and inside hundreds of the worlds top magazines, including GQ, Rolling Stone, Cosmopolitan, Glamour, Forbes, Business Week and TIME Magazine. And created portraits for advertising, multi international corporate annual reports, book publishing and private commissions.


Redel's work has won many awards, most notably from the Communication Arts Photography Annual. And his work has taken him to almost every state in the United States and many countries around the world.


Redel's passion for photography began during a dinner party his parents were giving. With the music of Sinatra and laughter in the air, a family friend who collected cars, cameras, guns and women, arrived with a beautiful blonde and a large silver case and casually emptied its contents onto his bed. Cameras, lenses and objects of all shapes and sizes lay everywhere to be played with. He was a kid in a candy store and awoke the next morning still holding a camera. He was seven years old.


During his early teens he enrolled in photography classes at school and quickly discovered a natural talent that earned him awards in photography contests. And it led to graduating High School with honors in Graphic Arts/Overall Performance. Living in a small town in Pennsylvania his exposure to commercial photography was through magazines, especially Harpers Bazaar, Vogue, Interview and GQ. It's on those pages that Redel discovered Richard Avedon, Hiro, Irving Penn and Bruce Weber’s work. Inspired, he applied and was accepted to the School of Visual Arts in New York City. And during his free time he worked as an assistant photographer to the same photographers whose work he admired. Redel quickly discovered that school wasn't any match for the real world practical experience he was gaining. And after his first year set off in a new and exciting direction and worked for the next four years for some of the most respected and famous photographers in the world.


Redel’s passion keeps him in demand and in his free time working on personal projects. Aside from his career, he's a professionally trained and skilled open wheel racecar driver. And a passionate road racing cyclist.


","textWidth":89}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false}}],"classicCategoryId":33454,"classicGuid":"4bd5ebf3a78e0","importSource":"classic-page","isHomePage":false},{"id":1748281880556,"title":"Contact","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"page_1748281880556","visible":true,"parentId":null,"siteId":"4bd5ebf3a78e0","slug":"contact","url":"/contact","pageElements":[{"id":1748281881556,"type":"metadata","title":"Metadata","visible":true,"position":0,"metaTitle":"Tel + 852 91717150","metaDescription":"Tel + 852 91717150","metaKeywords":"timothy redel, timothy redel contact, timothy redel fotography, hong kong fotographer,"},{"id":1748281882556,"type":"text","title":"Text Block Spacer","visible":true,"position":1,"textContent":"

 


","textWidth":100},{"id":1748281883556,"type":"column-container","title":"Imported Columns","visible":true,"position":2,"columns":[{"id":1748281883856,"hAlign":"right","vAlign":"middle","elements":[{"id":1748281883656,"type":"slideshow","title":"Image Slideshow","visible":true,"position":0,"slides":[{"imageUrl":"https://storage.neonsky.app/4bd5ebf3a78e0/images/TimothyRedel-6794.jpg","imageKey":"4bd5ebf3a78e0/images/TimothyRedel-6794.jpg","imageFilename":"TimothyRedel-6794.jpg","imageType":"image/jpeg"}],"slideDuration":5000,"transitionDuration":500,"slideshowWidth":88,"slideshowHeight":50,"showFullImages":true}]},{"id":1748281883956,"hAlign":"left","vAlign":"middle","elements":[{"id":1748281883756,"type":"text","title":"Text Block Content","visible":true,"position":0,"textContent":"


Timothy Redel Fotography - Hong Kong


Commissions - General Inquiries

Tele: +852 91717150

Emai: info@timothyredelfotography.com













","textWidth":82}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false}}],"classicCategoryId":2634,"classicGuid":"4bd5ebf3a78e0","importSource":"classic-page","isHomePage":false}]; let activeGalleryId = null; let sortableInstance = null; let isEditing = false; let initialPageInfo = null; // Store the current menu layout type let currentMenuLayout = null; document.addEventListener('DOMContentLoaded', () => { if (window.hydraInitialized) { console.log("Hydra already initialized, skipping"); return; } window.hydraInitialized = true; document.body.classList.add('hydra-initialized'); window.Parameters = window.Parameters || {}; if (localStorage.getItem('hydra_is_admin') === 'true') { document.body.classList.add('hydra-admin', 'hydra-authenticated'); isAdmin = true; isAuthenticated = true; console.log('Restored admin status from localStorage'); // CRITICAL FIX: Clear all caches except auth tokens for logged-in admins // This ensures admins always see fresh data when opening new tabs try { console.log('Admin detected - clearing all caches except auth tokens...'); // Save auth tokens first const savedAuthToken = localStorage.getItem('hydra_auth_token'); const savedAuthEmail = localStorage.getItem('hydra_auth_email'); const savedIsAdmin = localStorage.getItem('hydra_is_admin'); const savedDidToken = localStorage.getItem('hydra_did_token'); // Clear ALL localStorage localStorage.clear(); // Restore auth tokens if (savedAuthToken) localStorage.setItem('hydra_auth_token', savedAuthToken); if (savedAuthEmail) localStorage.setItem('hydra_auth_email', savedAuthEmail); if (savedIsAdmin) localStorage.setItem('hydra_is_admin', savedIsAdmin); if (savedDidToken) localStorage.setItem('hydra_did_token', savedDidToken); // Clear ALL sessionStorage (auth tokens are not stored here) sessionStorage.clear(); console.log('Cleared all caches except auth tokens for admin user'); } catch (cacheError) { console.warn('Error clearing caches on admin page load:', cacheError); // Don't fail page load if cache clearing fails } } function ensureSidebarElementsAndRender() { console.log('Ensuring sidebar elements are available before rendering...'); // Check if SidebarManager exists and has elements if (window.SidebarManager && (!window.SidebarManager.elements || window.SidebarManager.elements.length === 0)) { // Try to initialize from siteConfig if available if (typeof siteConfig !== 'undefined' && siteConfig.sitebarElements && siteConfig.sidebarElements.length > 0) { console.log('Initializing SidebarManager elements from siteConfig'); window.SidebarManager.elements = siteConfig.sidebarElements; } } // Determine layout and render let currentMenuLayout = 'sidebar'; if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) { currentMenuLayout = window.MenuStyleCustomizer.settings.menuLayout || 'sidebar'; } if (currentMenuLayout === 'horizontal') { console.log('Rendering horizontal menu with ensured sidebar elements'); renderHorizontalMenu(); } else { renderGalleries(); } } setTimeout(() => { console.log("Initializing galleries:", galleries.length); setTimeout(alignFolderStates, 100); // CHANGED: Use the new function instead of direct rendering setTimeout(ensureSidebarElementsAndRender, 100); // Small delay to ensure sidebar init initMobileMenu(); // Handle direct URL navigation if (initialPageInfo) { console.log("Loading initial page from direct URL:", initialPageInfo); // Find the gallery by ID const gallery = galleries.find(g => g.id === initialPageInfo.id); if (gallery) { // Set active gallery ID activeGalleryId = gallery.id; if (initialPageInfo.type === 'page') { console.log("Loading page directly:", gallery.title); loadPage(gallery.id); } else { console.log("Loading gallery directly:", gallery.title); loadGallery(gallery.id); } } } else if (window.location.pathname === '/') { // No specific path, try to load home page console.log("No specific path, checking for home page"); if (!loadHomePage() && galleries.length > 0) { // Default to first gallery if no home page is set loadGallery(galleries[0].id); } } else if (galleries.length > 0) { // Path exists but no matching initialPageInfo, try to handle URL path handleURLNavigation(); } // Log what was rendered console.log("Menu content:", document.getElementById('galleryTree')?.innerHTML || "Gallery tree not found"); // Start with edit controls properly set toggleEditControlsVisibility(isAdmin && document.body.classList.contains('edit-mode-active')); // Explicitly hide the import classic form too const importForm = document.getElementById('importClassicForm'); if (importForm) { importForm.style.display = 'none'; importForm.classList.remove('visible'); } // Try to restore token from localStorage if (!didToken) { didToken = localStorage.getItem('hydra_auth_token'); if (didToken) { console.log('Restored auth token from localStorage'); } } // Initialize Magic and check authentication // Initialize sidebar elements if available if (window.SidebarManager) { // Pass the initial elements from server config if available if (true) { window.SidebarManager.elements = [{"id":1743560579307,"type":"menu","title":"Menu","visible":true,"position":2},{"id":1743560590730,"type":"image","title":"Image","visible":true,"position":0,"imageData":"https://storage.neonsky.app/tma73fpp/1748281868362_Timothy_Redel.png","imageWidth":"100%","imageAlignment":"center"},{"id":1743595677459,"type":"social","title":"Social","visible":true,"position":4,"socialIconSize":16,"socialIcons":[{"type":"instagram","url":"https://instagram.com/edmonds.b/"}],"socialAlignment":"left"},{"id":1743597472848,"type":"image","title":"Image","visible":true,"position":1,"imageData":"https://storage.neonsky.app/fi8esmbe/1743597485060_spacer.gif","imageWidth":"10%","imageAlignment":"center"},{"id":1743597627682,"type":"image","title":"Image","visible":true,"position":3,"imageData":"https://storage.neonsky.app/fi8esmbe/1743597639626_spacer.gif","imageWidth":"48%","imageAlignment":"center"}]; } window.SidebarManager.init(); } // Initialize metadata from server config window.siteMetadata = {"title":"Timothy Redel Photography","description":"Timothy Redel is an American photographer based in Hong Kong.","googleAnalytics":" ","noIndex":false,"favicon":"","contentLanguage":"en","copyrightEnabled":false,"copyrightText":"","keywords":"","author":"timothyredelfotography.com","language":"en","locale":"en_US","siteName":"timothyredelfotography.com"}; console.log('Client Initialized site metadata (second init):', window.siteMetadata); console.log('Client Google Analytics from metadataToUse (second init):', ' '); console.log('Client Google Analytics in window.siteMetadata (second init):', window.siteMetadata.googleAnalytics); console.log('Client Content Language (second init):', window.siteMetadata.contentLanguage); // Update copyright footer after metadata is set (important for main domain) if (window.SidebarManager && typeof window.SidebarManager.updateMetadataFooter === 'function') { window.SidebarManager.updateMetadataFooter(); } const submenuCheck = document.getElementById('createSubmenu'); const submenuTitleField = document.getElementById('submenuTitle'); const submenuTitleGroup = submenuTitleField?.parentElement; if (submenuCheck && submenuTitleGroup) { submenuCheck.addEventListener('change', function() { submenuTitleGroup.style.display = this.checked ? 'block' : 'none'; }); } // Check authentication state after initialization if (isAuthenticated && isAdmin) { console.log('Already authenticated and admin, showing edit UI'); forceShowEditUI(); } else if (didToken) { console.log('Have token, checking admin status'); checkAuth(); } }, 100); // Small delay to ensure everything is loaded setTimeout(() => { if (window.location.pathname === '/' || (window.location.hostname === 'preview.neonsky.app' && window.location.pathname.split('/').length <= 2)) { // Root URL or preview URL with only GUID - check for home page if (!initialPageInfo) { // Only if no specific page was already loaded loadHomePage(); } } }, 500); }); /** * Gets the first visible sidebar element (image or text) to be used as a logo or title. * @returns {object|null} The first suitable sidebar element or null. */ function getFirstSidebarElementForHeader() { if (window.SidebarManager && window.SidebarManager.elements && window.SidebarManager.elements.length > 0) { const visibleElements = window.SidebarManager.elements.filter(el => el.visible !== false); // Filter 1: visible if (visibleElements.length > 0) { const sortedElements = visibleElements.sort((a, b) => a.position - b.position); for (let el of sortedElements) { if (el.type === 'image' || el.type === 'text') { // Filter 2: type return el; } } // If loop finishes, no visible image or text found among visible elements console.log('[getFirstSidebarElementForHeader] Found visible elements, but none were type image/text.'); return null; } else { console.log('[getFirstSidebarElementForHeader] No elements with visible !== false found in SidebarManager.elements.'); return null; } } console.log('[getFirstSidebarElementForHeader] SidebarManager, its elements, or elements array is empty/undefined.'); return null; } /** * Gets the first visible social icon element from sidebar elements. * @returns {object|null} The first suitable social element or null. */ function getFirstSocialElementForHeader() { if (window.SidebarManager && window.SidebarManager.elements && window.SidebarManager.elements.length > 0) { const visibleElements = window.SidebarManager.elements.filter(el => el.visible !== false); // Filter 1: visible if (visibleElements.length > 0) { const sortedElements = visibleElements.sort((a, b) => a.position - b.position); for (let el of sortedElements) { if (el.type === 'social') { // Filter for social type return el; } } // If loop finishes, no visible social element found console.log('[getFirstSocialElementForHeader] Found visible elements, but none were type social.'); return null; } else { console.log('[getFirstSocialElementForHeader] No elements with visible !== false found in SidebarManager.elements.'); return null; } } console.log('[getFirstSocialElementForHeader] SidebarManager, its elements, or elements array is empty/undefined.'); return null; } function ensureCorrectLayoutApplied() { // Check if MenuStyleCustomizer and its necessary parts exist if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings && typeof window.MenuStyleCustomizer._applyMenuLayout === 'function') { // Get the current layout setting, default to 'sidebar' if not set const currentLayout = window.MenuStyleCustomizer.settings.menuLayout || 'sidebar'; console.log(`Ensuring layout is applied: ${currentLayout}`); // Apply the layout styles (CSS classes, display properties) window.MenuStyleCustomizer._applyMenuLayout(currentLayout); // --- Force Header Structure Rebuild for Horizontal Layout --- if (currentLayout === 'horizontal') { const mobileHeader = document.querySelector('.mobile-header'); if (mobileHeader) { let mobileHeaderContent = mobileHeader.querySelector('.mobile-header-content'); // Ensure the main content container exists if (!mobileHeaderContent) { mobileHeaderContent = document.createElement('div'); mobileHeaderContent.className = 'mobile-header-content'; const hamburgerBtn = mobileHeader.querySelector('.hamburger-btn'); if (hamburgerBtn) { mobileHeader.insertBefore(mobileHeaderContent, hamburgerBtn); } else { mobileHeader.appendChild(mobileHeaderContent); } } // Ensure specific sub-containers exist, creating them if necessary // This guarantees the structure is present before renderHorizontalMenu fills them. if (!mobileHeaderContent.querySelector('.horizontal-header-logo')) { const logoContainer = document.createElement('div'); logoContainer.className = 'horizontal-header-logo'; mobileHeaderContent.insertBefore(logoContainer, mobileHeaderContent.firstChild); } if (!mobileHeaderContent.querySelector('.horizontal-menu-container')) { const menuContainer = document.createElement('div'); menuContainer.className = 'horizontal-menu-container'; const logoContainer = mobileHeaderContent.querySelector('.horizontal-header-logo'); if (logoContainer) { logoContainer.insertAdjacentElement('afterend', menuContainer); } else { // Fallback if logo container wasn't found/created mobileHeaderContent.insertBefore(menuContainer, mobileHeaderContent.firstChild); } } // Apply flexbox styling to position social container at far right mobileHeaderContent.style.display = 'flex'; mobileHeaderContent.style.alignItems = 'center'; mobileHeaderContent.style.justifyContent = 'space-between'; mobileHeaderContent.style.width = '100%'; // Create a flex container for logo and menu items const logoMenuContainer = mobileHeaderContent.querySelector('.logo-menu-container'); if (!logoMenuContainer) { console.log('[ensureCorrectLayoutApplied] Creating logo-menu wrapper...'); const logoMenuWrapper = document.createElement('div'); logoMenuWrapper.className = 'logo-menu-container'; logoMenuWrapper.style.display = 'flex'; logoMenuWrapper.style.alignItems = 'center'; logoMenuWrapper.style.gap = '20px'; // Move logo and menu into the wrapper const logoContainer = mobileHeaderContent.querySelector('.horizontal-header-logo'); const menuContainer = mobileHeaderContent.querySelector('.horizontal-menu-container'); console.log('[ensureCorrectLayoutApplied] Found logo container:', !!logoContainer); console.log('[ensureCorrectLayoutApplied] Found menu container:', !!menuContainer); if (logoContainer && menuContainer) { // Insert wrapper before the logo mobileHeaderContent.insertBefore(logoMenuWrapper, logoContainer); // Move both containers into the wrapper logoMenuWrapper.appendChild(logoContainer); logoMenuWrapper.appendChild(menuContainer); console.log('[ensureCorrectLayoutApplied] Successfully moved logo and menu into wrapper'); } else { console.warn('[ensureCorrectLayoutApplied] Missing logo or menu container, cannot create wrapper'); } } else { console.log('[ensureCorrectLayoutApplied] Logo-menu wrapper already exists'); } // Add social icons container for horizontal layout (AFTER logo-menu wrapper is created) if (!mobileHeaderContent.querySelector('.horizontal-social-container')) { const socialContainer = document.createElement('div'); socialContainer.className = 'horizontal-social-container'; // Insert at the end (far right) of the mobile header content mobileHeaderContent.appendChild(socialContainer); } } } // --- End Header Structure Rebuild --- // Re-render the correct menu structure based on the applied layout if (currentLayout === 'horizontal') { // If horizontal layout, render the horizontal menu if (typeof renderHorizontalMenu === 'function') { renderHorizontalMenu(); } else { console.warn("renderHorizontalMenu function not found."); } // After rendering horizontal menu, update mobile header content // This ensures mobile header shows logo instead of menu items on mobile setTimeout(() => { if (typeof updateMobileHeaderContent === 'function') { updateMobileHeaderContent(); } }, 100); } else { // For sidebar or top layouts, render the standard gallery tree if (typeof renderGalleries === 'function') { renderGalleries(); } else { console.warn("renderGalleries function not found."); } // Update mobile header content for other layouts too setTimeout(() => { if (typeof updateMobileHeaderContent === 'function') { updateMobileHeaderContent(); } }, 100); } } else { // Log a warning if the necessary components aren't available console.warn("MenuStyleCustomizer or its methods not available to re-apply layout."); } } function applyLayoutStylesOnly() { // Check if MenuStyleCustomizer and its necessary parts exist if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings && typeof window.MenuStyleCustomizer._applyMenuLayout === 'function') { // Get the current layout setting, default to 'sidebar' if not set const currentLayout = window.MenuStyleCustomizer.settings.menuLayout || 'sidebar'; console.log(`APPLY STYLES: Applying styles for layout: ${currentLayout}`); // Apply the layout styles (CSS classes, display properties) window.MenuStyleCustomizer._applyMenuLayout(currentLayout); // This function should ONLY set body classes/styles } else { console.warn("APPLY STYLES: MenuStyleCustomizer or its methods not available."); } } // Add this function to ensure sidebar content for mobile function ensureSidebarContentForMobile() { const isHorizontalLayout = document.body.classList.contains('menu-layout-horizontal'); const isMobileView = window.innerWidth <= 768; if (isHorizontalLayout && isMobileView) { console.log('Ensuring sidebar content for mobile horizontal layout'); // Make sure the sidebar tree is populated const galleryTreeContainer = document.getElementById('galleryTree'); if (galleryTreeContainer && (!galleryTreeContainer.innerHTML || galleryTreeContainer.innerHTML.trim() === '')) { console.log('Sidebar tree is empty, populating with galleries'); // Render the galleries in the sidebar tree for mobile navigation if (typeof renderGalleries === 'function') { renderGalleries(); } } // Also ensure sidebar elements are rendered if (window.SidebarManager && typeof window.SidebarManager.renderElements === 'function') { console.log('Re-rendering sidebar elements for mobile'); window.SidebarManager.renderElements(); } } } function renderTextLogo(container, htmlContentFromSidebarElement) { if (!container) { console.error("[RenderTextLogo] Error: Logo container not found."); return; } // Clear any existing content from the logo container. container.innerHTML = ''; // Directly inject the HTML content from the sidebar element. // This preserves the original HTML structure (like

, ) and any inline styles. // The .horizontal-header-logo CSS should handle alignment (e.g., display: flex; align-items: center;). container.innerHTML = htmlContentFromSidebarElement || '
Menu
'; // Fallback text wrapped in a div // Optional: If the root element of htmlContentFromSidebarElement (e.g., an

) // has default margins that interfere with flex centering, you might need CSS like: // .horizontal-header-logo > h1 { margin: 0; } // This would typically go in your menu-styles.css file. console.log(`[RenderTextLogo] Text logo HTML rendered directly into container.`); } function renderHorizontalMenu() { console.log("[RenderHorizontalMenu] Function CALLED. Current edit mode:", typeof isEditing !== 'undefined' ? isEditing : 'isEditing_undefined'); const mobileHeader = document.querySelector('.mobile-header'); if (!mobileHeader) { console.error("[RenderHorizontalMenu] CRITICAL: .mobile-header element NOT FOUND."); return; } // Apply flexbox styling to position social container at far right const mobileHeaderContent = mobileHeader.querySelector('.mobile-header-content'); if (mobileHeaderContent) { mobileHeaderContent.style.display = 'flex'; mobileHeaderContent.style.alignItems = 'center'; mobileHeaderContent.style.justifyContent = 'space-between'; mobileHeaderContent.style.width = '100%'; // Create a flex container for logo and menu items const logoMenuContainer = mobileHeaderContent.querySelector('.logo-menu-container'); if (!logoMenuContainer) { console.log('[RenderHorizontalMenu] Creating logo-menu wrapper...'); const logoMenuWrapper = document.createElement('div'); logoMenuWrapper.className = 'logo-menu-container'; logoMenuWrapper.style.display = 'flex'; logoMenuWrapper.style.alignItems = 'center'; logoMenuWrapper.style.gap = '20px'; // Move logo and menu into the wrapper const logoContainer = mobileHeaderContent.querySelector('.horizontal-header-logo'); const menuContainer = mobileHeaderContent.querySelector('.horizontal-menu-container'); console.log('[RenderHorizontalMenu] Found logo container:', !!logoContainer); console.log('[RenderHorizontalMenu] Found menu container:', !!menuContainer); if (logoContainer && menuContainer) { // Insert wrapper before the logo mobileHeaderContent.insertBefore(logoMenuWrapper, logoContainer); // Move both containers into the wrapper logoMenuWrapper.appendChild(logoContainer); logoMenuWrapper.appendChild(menuContainer); console.log('[RenderHorizontalMenu] Successfully moved logo and menu into wrapper'); } else { console.warn('[RenderHorizontalMenu] Missing logo or menu container, cannot create wrapper'); } } else { console.log('[RenderHorizontalMenu] Logo-menu wrapper already exists'); } } const logoContainer = mobileHeader.querySelector('.horizontal-header-logo'); if (!logoContainer) { console.error("[RenderHorizontalMenu] CRITICAL: .horizontal-header-logo container NOT FOUND within .mobile-header."); return; } console.log("[RenderHorizontalMenu] Found .horizontal-header-logo container"); logoContainer.innerHTML = ''; // Clear existing logo content first // FIXED: Check if we're on mobile - if so, don't render the horizontal logo const isMobileView = window.innerWidth <= 768; // Adjust breakpoint as needed if (isMobileView) { console.log("[RenderHorizontalMenu] Mobile view detected - skipping horizontal logo rendering"); // On mobile, the logo will be handled by updateMobileHeaderContent() } else { console.log("[RenderHorizontalMenu] Desktop view - rendering horizontal logo"); // Get the logo element data let logoElementData = null; try { logoElementData = getFirstSidebarElementForHeader(); console.log('[RenderHorizontalMenu] getFirstSidebarElementForHeader() returned:', JSON.stringify(logoElementData)); } catch (error) { console.error('[RenderHorizontalMenu] Error getting sidebar element:', error); logoElementData = getFirstSidebarElementFallback(); } if (logoElementData) { if (logoElementData.type === 'image' && (logoElementData.imageData || logoElementData.imageUrl)) { console.log('[RenderHorizontalMenu] Attempting to render IMAGE logo with src:', logoElementData.imageData || logoElementData.imageUrl); const img = document.createElement('img'); img.src = logoElementData.imageUrl || logoElementData.imageData; img.alt = logoElementData.title || 'Site Logo'; // Apply comprehensive styling to ensure visibility img.style.display = 'block'; // --- FIX --- // The line below was removed. It was applying a fixed max-height of 50px, // overriding your CSS. Removing it allows your stylesheet to take control. // img.style.maxHeight = '50px'; img.style.width = 'auto'; img.style.objectFit = 'contain'; img.style.minWidth = '1px'; img.style.minHeight = '1px'; img.style.visibility = 'visible'; img.style.opacity = '1'; // Enhanced event listeners for debugging img.onload = () => { console.log('[RenderHorizontalMenu] Image successfully LOADED'); }; img.onerror = (error) => { console.error('[RenderHorizontalMenu] Image FAILED to load:', error); renderTextLogo(logoContainer, logoElementData.title || 'Logo'); }; try { // Handle linking (similar to sidebar-manager.js) // Only add links in view mode, not edit mode const isEditing = window.SidebarManager?.isEditing || document.body.classList.contains('edit-mode-active') || (typeof window.isInEditMode === 'function' && window.isInEditMode()); if (logoElementData.linkType && logoElementData.linkType !== 'none' && !isEditing) { const link = document.createElement('a'); link.target = logoElementData.linkTarget || '_self'; let finalLinkHref = '#'; let isValidLink = false; if (logoElementData.linkType === 'external' && logoElementData.linkUrl) { finalLinkHref = logoElementData.linkUrl; if (link.target === '_blank') { link.rel = 'noopener noreferrer'; } isValidLink = true; } else if (logoElementData.linkType === 'internal' && logoElementData.linkPageId) { const allGalleries = window.galleries || (typeof galleries !== 'undefined' ? galleries : []); const targetPage = allGalleries.find(g => g.id === parseInt(logoElementData.linkPageId)); if (targetPage) { // Handle preview mode URLs const isPreviewMode = window.location.hostname === 'preview.neonsky.app'; if (isPreviewMode) { const pathParts = window.location.pathname.split('/').filter(Boolean); const siteGuid = pathParts[0]; if (siteGuid && targetPage.slug) { finalLinkHref = '/' + siteGuid + '/' + targetPage.slug; isValidLink = true; } else if (targetPage.url && targetPage.url.startsWith('/')) { finalLinkHref = targetPage.url; isValidLink = true; } } else { if (targetPage.slug) { finalLinkHref = '/' + targetPage.slug; isValidLink = true; } else if (targetPage.url && targetPage.url.startsWith('/')) { finalLinkHref = targetPage.url; isValidLink = true; } } } } if (isValidLink) { link.href = finalLinkHref; // For internal links, add click handler to use site navigation if (logoElementData.linkType === 'internal' && logoElementData.linkPageId) { link.addEventListener('click', (e) => { e.preventDefault(); const allGalleries = window.galleries || (typeof galleries !== 'undefined' ? galleries : []); const targetPage = allGalleries.find(g => g.id === parseInt(logoElementData.linkPageId)); if (targetPage) { if (targetPage.isPage && typeof window.loadPage === 'function') { window.loadPage(targetPage.id); } else if (typeof window.loadGallery === 'function') { window.loadGallery(targetPage.id); } else { // Fallback to standard navigation window.location.href = finalLinkHref; } } else { // Fallback to standard navigation if page not found window.location.href = finalLinkHref; } }); } link.appendChild(img); logoContainer.appendChild(link); console.log('[RenderHorizontalMenu] SUCCESSFULLY appended linked image to logoContainer.'); } else { logoContainer.appendChild(img); console.log('[RenderHorizontalMenu] SUCCESSFULLY appended image to logoContainer.'); } } else { logoContainer.appendChild(img); console.log('[RenderHorizontalMenu] SUCCESSFULLY appended image to logoContainer.'); } } catch (e) { console.error('[RenderHorizontalMenu] Error during logoContainer.appendChild(img):', e); renderTextLogo(logoContainer, logoElementData.title || 'Logo'); } } else if (logoElementData.type === 'text' && logoElementData.textContent) { console.log('[RenderHorizontalMenu] Attempting to render TEXT logo:', logoElementData.textContent); renderTextLogo(logoContainer, logoElementData.textContent); } else { console.log('[RenderHorizontalMenu] Using title as fallback logo'); renderTextLogo(logoContainer, logoElementData.title || 'Menu'); } } else { console.log("[RenderHorizontalMenu] No logoElementData available. Using default text."); renderTextLogo(logoContainer, 'Menu'); } } // Render menu items (only on desktop, not on mobile) const menuContainer = mobileHeader.querySelector('.horizontal-menu-container'); if (!menuContainer) { console.error("[RenderHorizontalMenu] .horizontal-menu-container NOT FOUND within .mobile-header."); } else { menuContainer.innerHTML = ''; // Only populate menu items on desktop, not on mobile if (!isMobileView) { let galleryTree = []; if (typeof createGalleryTree === 'function' && typeof galleries !== 'undefined') { galleryTree = createGalleryTree(galleries); } else { console.warn("[RenderHorizontalMenu] createGalleryTree function or galleries array is undefined."); } galleryTree.forEach(item => { const menuItemElement = createHorizontalMenuItem(item); if (menuItemElement) { menuContainer.appendChild(menuItemElement); } }); } else { console.log("[RenderHorizontalMenu] Mobile view - skipping horizontal menu item population"); } } // Render social icons (only on desktop, not on mobile) let socialContainer = mobileHeader.querySelector('.horizontal-social-container'); if (!socialContainer) { // Fallback: create the social container if it doesn't exist console.log("[RenderHorizontalMenu] .horizontal-social-container not found, creating it..."); const mobileHeaderContent = mobileHeader.querySelector('.mobile-header-content'); if (mobileHeaderContent) { socialContainer = document.createElement('div'); socialContainer.className = 'horizontal-social-container'; mobileHeaderContent.appendChild(socialContainer); console.log("[RenderHorizontalMenu] Created .horizontal-social-container"); } else { console.error("[RenderHorizontalMenu] .mobile-header-content not found, cannot create social container"); } } if (socialContainer) { socialContainer.innerHTML = ''; // Only populate social icons on desktop, not on mobile if (!isMobileView) { const socialElement = getFirstSocialElementForHeader(); if (socialElement && socialElement.type === 'social') { console.log('[RenderHorizontalMenu] Found social element, rendering social icons'); renderSocialIcons(socialContainer, socialElement); } else { console.log('[RenderHorizontalMenu] No social element found or element is not social type'); // Container remains empty - no impact on layout } } else { console.log("[RenderHorizontalMenu] Mobile view - skipping social icons population"); } } if (typeof updateActiveStatesHorizontal === 'function') { updateActiveStatesHorizontal(); } setTimeout(() => { ensureSidebarContentForMobile(); }, 100); console.log("[RenderHorizontalMenu] Function COMPLETED."); } /** * Renders social icons in the horizontal header container. * @param {HTMLElement} container - The container to render social icons in * @param {object} socialElement - The social element data from sidebar */ function renderSocialIcons(container, socialElement) { console.log('[RenderSocialIcons] Rendering social icons:', socialElement); try { // Safety check: ensure we have a valid social element if (!socialElement || socialElement.type !== 'social') { console.log('[RenderSocialIcons] Invalid social element - skipping rendering'); return; } // Find the existing sidebar social icons and clone them const sidebarSocialContainer = document.querySelector('.sidebar-social-container'); if (sidebarSocialContainer) { const sidebarSocialLinks = sidebarSocialContainer.querySelectorAll('a[href]'); sidebarSocialLinks.forEach(link => { // Clone the entire link element with all its styling and functionality const clonedLink = link.cloneNode(true); // Remove any edit-specific classes or attributes clonedLink.classList.remove('edit-social-icon', 'delete-social-icon'); // Ensure it opens in new tab clonedLink.target = '_blank'; clonedLink.rel = 'noopener noreferrer'; // Add horizontal menu specific class clonedLink.classList.add('horizontal-social-link'); // Apply horizontal menu specific styling - let CSS handle most styling clonedLink.style.cssText = 'display: flex !important; align-items: center !important; justify-content: center !important; width: 30px !important; height: 30px !important; color: var(--menu-color, #333) !important; text-decoration: none !important; flex-shrink: 0 !important;'; // Style the icon wrapper to match horizontal menu sizing const iconWrapper = clonedLink.querySelector('.social-icon-wrapper'); if (iconWrapper) { iconWrapper.style.cssText = 'display: flex !important; align-items: center !important; justify-content: center !important;'; } // Style the SVG to use proper sizing const svg = clonedLink.querySelector('svg'); if (svg) { svg.style.width = '20px'; svg.style.height = '20px'; svg.setAttribute('width', '20px'); svg.setAttribute('height', '20px'); svg.setAttribute('preserveAspectRatio', 'xMidYMid meet'); svg.style.color = 'inherit'; svg.style.fill = 'currentColor'; svg.style.stroke = 'currentColor'; } // Remove any click handlers that might interfere const newLink = clonedLink.cloneNode(true); container.appendChild(newLink); console.log('[RenderSocialIcons] Cloned sidebar social link:', newLink.href); }); } else { console.log('[RenderSocialIcons] No sidebar social container found to clone from'); } } catch (error) { console.error('[RenderSocialIcons] Error rendering social icons:', error); } } /** * Gets the appropriate icon text or class for a social platform. * @param {string} platform - The social platform name * @returns {string} The icon text or class */ function getSocialIconText(platform) { const iconMap = { 'facebook': 'f', 'twitter': 't', 'instagram': 'i', 'linkedin': 'in', 'youtube': 'yt', 'pinterest': 'p', 'tiktok': 'tt', 'snapchat': 'sc', 'whatsapp': 'wa', 'telegram': 'tg', 'discord': 'd', 'github': 'gh', 'email': '@', 'phone': '📞', 'website': '🌐' }; return iconMap[platform.toLowerCase()] || '•'; } /** * Creates a single top-level horizontal menu item and its submenu if it has children. * (UPDATED to handle async loadGallery and pass clickedItemId to a delayed updateActiveStatesHorizontal) * @param {object} galleryItem - The gallery item data. * @returns {HTMLElement | null} The created menu item element, or null if not rendered. */ function createHorizontalMenuItem(galleryItem) { if (galleryItem.visible === false || galleryItem.isSpacer) { return null; } const menuItem = document.createElement('div'); menuItem.className = 'horizontal-menu-item'; menuItem.dataset.id = String(galleryItem.id); menuItem.setAttribute('data-visible', galleryItem.visible !== false ? 'true' : 'false'); const titleSpan = document.createElement('span'); titleSpan.textContent = galleryItem.title; menuItem.appendChild(titleSpan); // Ensure activeGalleryId is treated as a number for comparison during initial render const currentGlobalActiveId = parseInt(String(window.activeGalleryId || activeGalleryId)); if (galleryItem.id === currentGlobalActiveId) { menuItem.classList.add('active'); } let leaveTimer; // For mouseleave timeout to close dropdown if (galleryItem.children && galleryItem.children.length > 0) { const visibleChildren = galleryItem.children.filter(child => child.visible !== false && !child.isSpacer); if (visibleChildren.length > 0) { menuItem.classList.add('has-children'); const toggleIconContainer = document.createElement('span'); toggleIconContainer.className = 'submenu-toggle-horizontal'; const svgIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svgIcon.setAttribute("class", "icon toggle-icon"); svgIcon.setAttribute("viewBox", "0 0 24 24"); svgIcon.setAttribute("fill", "none"); svgIcon.setAttribute("stroke", "currentColor"); svgIcon.setAttribute("stroke-width", "2"); const polyline = document.createElementNS("http://www.w3.org/2000/svg", "polyline"); polyline.setAttribute("points", "9 6 15 12 9 18"); // Right-pointing arrow svgIcon.appendChild(polyline); toggleIconContainer.appendChild(svgIcon); menuItem.appendChild(toggleIconContainer); const subMenu = document.createElement('div'); subMenu.className = 'horizontal-dropdown'; visibleChildren.forEach((childItem) => { const subMenuItemElement = createHorizontalSubMenuItem(childItem); // Recursive call if (subMenuItemElement) { subMenu.appendChild(subMenuItemElement); } }); if (subMenu.hasChildNodes()) { menuItem.appendChild(subMenu); menuItem.addEventListener('mouseenter', function() { clearTimeout(leaveTimer); document.querySelectorAll('.horizontal-menu-item.expanded').forEach((openItem) => { if (openItem !== this) { openItem.classList.remove('expanded'); const otherIcon = openItem.querySelector('.icon.toggle-icon'); if (otherIcon) otherIcon.classList.remove('rotated'); } }); this.classList.add('expanded'); const currentIcon = this.querySelector('.icon.toggle-icon'); if (currentIcon) currentIcon.classList.add('rotated'); }); menuItem.addEventListener('mouseleave', function(event) { // Check if we're moving to the dropdown or a flyout const relatedTarget = event.relatedTarget; const isMovingToDropdown = relatedTarget && ( relatedTarget.closest('.horizontal-dropdown') || relatedTarget.classList.contains('horizontal-submenu-item') || relatedTarget.closest('.horizontal-submenu-item') ); const isMovingToFlyout = relatedTarget && ( relatedTarget.classList.contains('horizontal-nested-dropdown') || relatedTarget.closest('.horizontal-nested-dropdown') ); if (!isMovingToDropdown && !isMovingToFlyout) { console.log('Main menu item mouseleave - starting timer'); leaveTimer = setTimeout(() => { // Check if there's an active flyout - if so, keep parent open // Use the same multiple detection methods as the submenu handler const activeFlyout = document.querySelector('.horizontal-nested-dropdown[style*="display: block"]') || document.querySelector('.horizontal-nested-dropdown[style*="visibility: visible"]') || document.querySelector('.horizontal-nested-dropdown:not([style*="display: none"])'); console.log('Main menu timer fired - active flyout:', activeFlyout); if (!activeFlyout) { console.log('Main menu closing dropdown - no active flyout'); this.classList.remove('expanded'); const currentIcon = this.querySelector('.icon.toggle-icon'); if (currentIcon) currentIcon.classList.remove('rotated'); } else { console.log('Main menu keeping dropdown open - flyout is active'); } }, 200); } else { console.log('Main menu item mouseleave - not starting timer (moving to dropdown or flyout)'); } }); subMenu.addEventListener('mouseenter', function() { const parentMenuItem = this.closest('.horizontal-menu-item'); if (parentMenuItem && parentMenuItem.leaveTimer) { clearTimeout(parentMenuItem.leaveTimer); parentMenuItem.leaveTimer = null; } }); subMenu.addEventListener('mouseleave', function(event) { const parentMenuItem = this.closest('.horizontal-menu-item'); if (parentMenuItem) { // Check if we're moving to another element within the same dropdown const relatedTarget = event.relatedTarget; console.log('SubMenu mouseleave - relatedTarget:', relatedTarget); console.log('SubMenu mouseleave - relatedTarget classes:', relatedTarget ? relatedTarget.className : 'null'); // Check if we're moving to another element within the same dropdown const currentDropdown = this.closest('.horizontal-dropdown') || this.closest('.horizontal-submenu')?.closest('.horizontal-dropdown'); console.log('Current dropdown found:', !!currentDropdown); console.log('Current dropdown element:', currentDropdown); console.log('RelatedTarget:', relatedTarget); console.log('RelatedTarget closest dropdown:', relatedTarget ? relatedTarget.closest('.horizontal-dropdown') : 'null'); // Check if relatedTarget is within the same dropdown container const isMovingWithinDropdown = relatedTarget && currentDropdown && ( relatedTarget.closest('.horizontal-dropdown') === currentDropdown || relatedTarget.classList.contains('horizontal-submenu-item') || relatedTarget.closest('.horizontal-submenu-item') || currentDropdown.contains(relatedTarget) ); // Alternative approach: check if relatedTarget is within the same parent menu item const parentMenuContainer = this.closest('.horizontal-menu-item'); const isMovingWithinParent = relatedTarget && parentMenuContainer && parentMenuContainer.contains(relatedTarget); console.log('Is moving within parent menu item:', isMovingWithinParent); console.log('Current dropdown contains relatedTarget:', currentDropdown ? currentDropdown.contains(relatedTarget) : 'no dropdown'); console.log('RelatedTarget closest dropdown === currentDropdown:', relatedTarget && currentDropdown ? relatedTarget.closest('.horizontal-dropdown') === currentDropdown : 'null'); // Also check if mouse is still within the dropdown bounds (in case relatedTarget is null or outside) // Get the parent menu item's dropdown container const parentMenuItem = this.closest('.horizontal-menu-item'); console.log('Parent menu item found:', !!parentMenuItem); const dropdownContainer = parentMenuItem ? parentMenuItem.querySelector('.horizontal-dropdown') : null; console.log('Dropdown container found:', !!dropdownContainer); if (dropdownContainer) { console.log('Dropdown container classes:', dropdownContainer.className); console.log('Dropdown container display:', window.getComputedStyle(dropdownContainer).display); console.log('Dropdown container position:', window.getComputedStyle(dropdownContainer).position); } // Get bounds from the visible dropdown (not the hidden one) let dropdownRect = { left: 0, right: 0, top: 0, bottom: 0 }; if (dropdownContainer) { const computedStyle = window.getComputedStyle(dropdownContainer); if (computedStyle.display !== 'none') { dropdownRect = dropdownContainer.getBoundingClientRect(); } else { // If dropdown is hidden, try to get bounds from the parent menu item const parentRect = parentMenuItem.getBoundingClientRect(); dropdownRect = parentRect; } } const mouseX = event.clientX; const mouseY = event.clientY; const isMouseInDropdownBounds = mouseX >= dropdownRect.left && mouseX <= dropdownRect.right && mouseY >= dropdownRect.top && mouseY <= dropdownRect.bottom; console.log('Dropdown bounds:', { left: dropdownRect.left, right: dropdownRect.right, top: dropdownRect.top, bottom: dropdownRect.bottom, mouseX, mouseY, mouseInBounds: mouseX >= dropdownRect.left && mouseX <= dropdownRect.right && mouseY >= dropdownRect.top && mouseY <= dropdownRect.bottom }); // If there's an active flyout, always keep the parent dropdown open // Check multiple ways to detect active flyouts const activeFlyout = document.querySelector('.horizontal-nested-dropdown[style*="display: block"]') || document.querySelector('.horizontal-nested-dropdown[style*="visibility: visible"]') || document.querySelector('.horizontal-nested-dropdown:not([style*="display: none"])'); console.log('Active flyout found:', !!activeFlyout); if (activeFlyout) { console.log('Active flyout display style:', activeFlyout.style.display); console.log('Active flyout computed display:', window.getComputedStyle(activeFlyout).display); console.log('Active flyout visibility style:', activeFlyout.style.visibility); } const hasActiveFlyout = !!activeFlyout; // If the dropdown is hidden (display: none), we can't get accurate bounds // So let's use a simpler approach - if we're moving to another submenu item, keep open const isMovingToSubmenuItem = relatedTarget && ( relatedTarget.classList.contains('horizontal-submenu-item') || relatedTarget.closest('.horizontal-submenu-item') ); console.log('Is moving to submenu item:', isMovingToSubmenuItem); console.log('RelatedTarget is submenu item:', relatedTarget ? relatedTarget.classList.contains('horizontal-submenu-item') : 'null'); console.log('RelatedTarget has submenu item ancestor:', relatedTarget ? !!relatedTarget.closest('.horizontal-submenu-item') : 'null'); // If we're moving to another submenu item within the same dropdown, always keep open // OR if there's an active flyout (regardless of where the mouse is), keep open // Simple approach: if mouse is within dropdown bounds OR there's an active flyout, keep open const shouldKeepOpen = isMouseInDropdownBounds || hasActiveFlyout || isMovingWithinDropdown || isMovingToSubmenuItem; console.log('Is moving within dropdown:', isMovingWithinDropdown); console.log('Is mouse in dropdown bounds:', isMouseInDropdownBounds); console.log('Has active flyout:', hasActiveFlyout); console.log('Should keep open:', shouldKeepOpen); if (!shouldKeepOpen) { console.log('SubMenu mouseleave - starting timer (moving outside dropdown)'); parentMenuItem.leaveTimer = setTimeout(() => { // Check if there's an active flyout - if so, keep parent open const activeFlyout = document.querySelector('.horizontal-nested-dropdown[style*="display: block"]'); console.log('Timer fired - active flyout:', activeFlyout); if (!activeFlyout) { console.log('Closing parent dropdown - no active flyout'); parentMenuItem.classList.remove('expanded'); const currentIcon = parentMenuItem.querySelector('.icon.toggle-icon'); if (currentIcon) currentIcon.classList.remove('rotated'); } else { console.log('Keeping parent dropdown open - flyout is active'); } }, 200); } else { console.log('SubMenu mouseleave - not starting timer (moving within dropdown)'); } } }); } else { menuItem.classList.remove('has-children'); if (toggleIconContainer.parentNode) { toggleIconContainer.remove(); } } } } // Click listener for items that are direct navigation links (not folders with children) if (!menuItem.classList.contains('has-children')) { menuItem.addEventListener('click', async (event) => { event.stopPropagation(); const clickedItemId = galleryItem.id; // Capture the ID of the clicked item // Check if it's an external URL first if (galleryItem.isExternal && galleryItem.url) { window.open(galleryItem.url, '_blank', 'noopener,noreferrer'); return; } if (galleryItem.isPage) { if (typeof loadPage === 'function') { loadPage(clickedItemId, event); // loadPage is synchronous if (typeof updateActiveStatesHorizontal === 'function') { console.log("Click Handler (Page): Calling updateActiveStatesHorizontal for ID:", clickedItemId); // Use a minimal delay even for sync operations if page rendering has its own async microtasks setTimeout(() => updateActiveStatesHorizontal(clickedItemId), 50); } } } else { // It's a gallery if (typeof loadGallery === 'function') { console.log("Click Handler (Gallery): Awaiting loadGallery for ID:", clickedItemId); await loadGallery(clickedItemId, event); // Await the asynchronous loadGallery console.log("Click Handler (Gallery): loadGallery completed for ID:", clickedItemId, ". Calling updateActiveStatesHorizontal with delay."); if (typeof updateActiveStatesHorizontal === 'function') { setTimeout(() => { console.log("[Delayed Update from Click] Calling updateActiveStatesHorizontal for gallery ID:", clickedItemId); updateActiveStatesHorizontal(clickedItemId); // Pass the captured clickedItemId }, 100); // Delay to allow gallery system to settle } } } // Close any other open dropdowns and flyouts document.querySelectorAll('.horizontal-menu-item.expanded').forEach((openItem) => { // Don't close the parent if a child within its dropdown was clicked (already handled by submenu click) if (openItem !== menuItem.closest('.horizontal-menu-item.expanded')) { openItem.classList.remove('expanded'); const icon = openItem.querySelector('.icon.toggle-icon'); if (icon) icon.classList.remove('rotated'); } }); // Close all flyouts document.querySelectorAll('.horizontal-nested-dropdown').forEach((flyout) => { flyout.style.display = 'none'; flyout.style.opacity = '0'; flyout.style.visibility = 'hidden'; }); // Remove expanded class from all submenu items document.querySelectorAll('.horizontal-submenu-item.expanded').forEach((subItem) => { subItem.classList.remove('expanded'); }); }); } return menuItem; } /** * Creates a single horizontal submenu item. * (UPDATED to handle nested children and async loadGallery) * @param {object} galleryItem - The gallery item data for the submenu. * @returns {HTMLElement | null} The created submenu item element, or null if not rendered. */ function createHorizontalSubMenuItem(galleryItem) { if (galleryItem.visible === false || galleryItem.isSpacer) { return null; } const subMenuItem = document.createElement('li'); subMenuItem.className = 'horizontal-submenu-item'; subMenuItem.dataset.id = String(galleryItem.id); subMenuItem.setAttribute('data-visible', galleryItem.visible !== false ? 'true' : 'false'); // Create the text content const titleSpan = document.createElement('span'); titleSpan.textContent = galleryItem.title; subMenuItem.appendChild(titleSpan); const currentGlobalActiveId = parseInt(String(window.activeGalleryId || activeGalleryId)); if (galleryItem.id === currentGlobalActiveId) { subMenuItem.classList.add('active'); } // Check if this submenu item has children (nested sub-sub items) if (galleryItem.children && galleryItem.children.length > 0) { const visibleChildren = galleryItem.children.filter(child => child.visible !== false && !child.isSpacer); if (visibleChildren.length > 0) { subMenuItem.classList.add('has-children'); // Add arrow indicator for nested items (same as top-level menu) const arrowSpan = document.createElement('span'); arrowSpan.className = 'submenu-arrow'; const svgIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svgIcon.setAttribute("class", "icon toggle-icon"); svgIcon.setAttribute("viewBox", "0 0 24 24"); svgIcon.setAttribute("fill", "none"); svgIcon.setAttribute("stroke", "currentColor"); svgIcon.setAttribute("stroke-width", "2"); const polyline = document.createElementNS("http://www.w3.org/2000/svg", "polyline"); polyline.setAttribute("points", "9 6 15 12 9 18"); // Right-pointing arrow svgIcon.appendChild(polyline); arrowSpan.appendChild(svgIcon); subMenuItem.appendChild(arrowSpan); // Create nested submenu as a true flyout const nestedSubMenu = document.createElement('div'); nestedSubMenu.className = 'horizontal-nested-dropdown'; // Create a list container for the nested items const nestedList = document.createElement('ul'); nestedList.className = 'horizontal-nested-list'; visibleChildren.forEach((childItem) => { const nestedSubMenuItem = createHorizontalSubMenuItem(childItem); // Recursive call if (nestedSubMenuItem) { nestedList.appendChild(nestedSubMenuItem); } }); nestedSubMenu.appendChild(nestedList); if (nestedSubMenu.hasChildNodes()) { // Append to document body for true flyout behavior document.body.appendChild(nestedSubMenu); // Store reference to the flyout for cleanup const flyoutId = 'flyout-' + Date.now() + '-' + Math.random(); subMenuItem.setAttribute('data-flyout-id', flyoutId); nestedSubMenu.setAttribute('data-flyout-id', flyoutId); console.log('Created flyout with ID:', flyoutId, 'for item:', galleryItem.title); // Debug console.log('Flyout element:', nestedSubMenu); // Debug // Add hover events for nested submenu let nestedLeaveTimer; subMenuItem.addEventListener('mouseenter', function() { if (this.nestedLeaveTimer) { clearTimeout(this.nestedLeaveTimer); } // Keep the parent dropdown open by preventing its mouseleave timer const parentMenuItem = this.closest('.horizontal-menu-item'); if (parentMenuItem) { // Clear any existing leave timer on the parent if (parentMenuItem.leaveTimer) { clearTimeout(parentMenuItem.leaveTimer); } } // Close other nested dropdowns document.querySelectorAll('.horizontal-submenu-item.expanded').forEach((openItem) => { if (openItem !== this) { openItem.classList.remove('expanded'); // Hide other flyouts const otherFlyoutId = openItem.getAttribute('data-flyout-id'); if (otherFlyoutId) { const otherFlyout = document.querySelector('[data-flyout-id="' + otherFlyoutId + '"]'); if (otherFlyout) { otherFlyout.style.display = 'none'; otherFlyout.style.opacity = '0'; otherFlyout.style.visibility = 'hidden'; } } } }); this.classList.add('expanded'); // Show and position the nested dropdown as a true flyout const flyoutId = this.getAttribute('data-flyout-id'); console.log('Flyout ID:', flyoutId); // Debug if (flyoutId) { // Find the actual flyout container (not the submenu item) const nestedDropdown = document.querySelector('.horizontal-nested-dropdown[data-flyout-id="' + flyoutId + '"]'); console.log('Found nested dropdown:', nestedDropdown); // Debug if (nestedDropdown) { const rect = this.getBoundingClientRect(); const parentDropdown = this.closest('.horizontal-dropdown'); const parentRect = parentDropdown ? parentDropdown.getBoundingClientRect() : rect; console.log('Positioning flyout - rect:', rect, 'parentRect:', parentRect); // Debug // Position to the right of the parent dropdown (touching) const leftPos = parentRect.right - 1; // Slight overlap to eliminate gap const topPos = rect.top; nestedDropdown.style.left = leftPos + 'px'; nestedDropdown.style.top = topPos + 'px'; nestedDropdown.style.display = 'block'; nestedDropdown.style.opacity = '1'; nestedDropdown.style.visibility = 'visible'; nestedDropdown.style.position = 'fixed'; // Ensure fixed positioning nestedDropdown.style.zIndex = '1251'; // Ensure high z-index console.log('Flyout shown - display: block, opacity: 1, visibility: visible'); console.log('Set flyout position:', leftPos, topPos); // Debug // Check if it would go off-screen and adjust if needed setTimeout(() => { const dropdownRect = nestedDropdown.getBoundingClientRect(); if (dropdownRect.right > window.innerWidth) { // Position to the left of the parent dropdown instead const newLeftPos = parentRect.left - dropdownRect.width - 5; nestedDropdown.style.left = newLeftPos + 'px'; console.log('Adjusted flyout position (left side):', newLeftPos); // Debug } }, 10); } else { console.error('Nested dropdown not found for flyout ID:', flyoutId); // Debug } } else { console.error('No flyout ID found for submenu item'); // Debug } }); subMenuItem.addEventListener('mouseleave', function(event) { // Check if we're moving to the flyout const flyoutId = this.getAttribute('data-flyout-id'); const relatedTarget = event.relatedTarget; const isMovingToFlyout = relatedTarget && flyoutId && relatedTarget.closest('.horizontal-nested-dropdown[data-flyout-id="' + flyoutId + '"]'); // Also check if we're moving within the dropdown const parentMenuItem = this.closest('.horizontal-menu-item'); const isMovingWithinDropdown = relatedTarget && parentMenuItem && parentMenuItem.contains(relatedTarget); console.log('Submenu item mouseleave - moving to flyout:', isMovingToFlyout); console.log('Submenu item mouseleave - moving within dropdown:', isMovingWithinDropdown); if (!isMovingToFlyout && !isMovingWithinDropdown) { this.nestedLeaveTimer = setTimeout(() => { this.classList.remove('expanded'); // Hide the flyout if (flyoutId) { const nestedDropdown = document.querySelector('.horizontal-nested-dropdown[data-flyout-id="' + flyoutId + '"]'); if (nestedDropdown) { nestedDropdown.style.display = 'none'; nestedDropdown.style.opacity = '0'; nestedDropdown.style.visibility = 'hidden'; } } // Now that the flyout is closed, allow the parent dropdown to close const parentMenuItem = this.closest('.horizontal-menu-item'); if (parentMenuItem) { setTimeout(() => { const activeFlyout = document.querySelector('.horizontal-nested-dropdown[style*="display: block"]'); if (!activeFlyout) { parentMenuItem.classList.remove('expanded'); const currentIcon = parentMenuItem.querySelector('.icon.toggle-icon'); if (currentIcon) currentIcon.classList.remove('rotated'); } }, 50); } }, 200); } else { console.log('Submenu item mouseleave - not starting timer (moving to flyout)'); } }); nestedSubMenu.addEventListener('mouseenter', function() { console.log('Flyout mouseenter - clearing timers'); clearTimeout(nestedLeaveTimer); // Clear the parent dropdown's leave timer to keep it open const flyoutId = this.getAttribute('data-flyout-id'); if (flyoutId) { const parentSubItem = document.querySelector('.horizontal-submenu-item[data-flyout-id="' + flyoutId + '"]'); if (parentSubItem) { const parentMenuItem = parentSubItem.closest('.horizontal-menu-item'); if (parentMenuItem && parentMenuItem.leaveTimer) { console.log('Clearing parent menu item leave timer'); clearTimeout(parentMenuItem.leaveTimer); parentMenuItem.leaveTimer = null; } else { console.log('No parent menu item leave timer to clear'); } if (parentSubItem.nestedLeaveTimer) { console.log('Clearing submenu item nested leave timer'); clearTimeout(parentSubItem.nestedLeaveTimer); parentSubItem.nestedLeaveTimer = null; } } } }); nestedSubMenu.addEventListener('mouseleave', function() { const flyoutId = this.getAttribute('data-flyout-id'); if (flyoutId) { const parentSubItem = document.querySelector('.horizontal-submenu-item[data-flyout-id="' + flyoutId + '"]'); if (parentSubItem) { nestedLeaveTimer = setTimeout(() => { parentSubItem.classList.remove('expanded'); this.style.display = 'none'; this.style.opacity = '0'; this.style.visibility = 'hidden'; }, 200); } } }); } } } // Click listener for navigation (only if no children or if it's a direct link) if (!subMenuItem.classList.contains('has-children') || galleryItem.isPage) { subMenuItem.addEventListener('click', async (event) => { event.stopPropagation(); const clickedItemId = galleryItem.id; // Check if it's an external URL first if (galleryItem.isExternal && galleryItem.url) { window.open(galleryItem.url, '_blank', 'noopener,noreferrer'); return; } if (galleryItem.isPage) { if (typeof loadPage === 'function') { loadPage(clickedItemId, event); if (typeof updateActiveStatesHorizontal === 'function') { console.log("SubMenu Click Handler (Page): Calling updateActiveStatesHorizontal for ID:", clickedItemId); setTimeout(() => updateActiveStatesHorizontal(clickedItemId), 50); } } } else { // It's a gallery if (typeof loadGallery === 'function') { console.log("SubMenu Click Handler (Gallery): Awaiting loadGallery for ID:", clickedItemId); await loadGallery(clickedItemId, event); console.log("SubMenu Click Handler (Gallery): loadGallery completed for ID:", clickedItemId, ". Calling updateActiveStatesHorizontal with delay."); if (typeof updateActiveStatesHorizontal === 'function') { setTimeout(() => { console.log("[Delayed Update from SubMenu Click] Calling updateActiveStatesHorizontal for gallery ID:", clickedItemId); updateActiveStatesHorizontal(clickedItemId); }, 100); } } } // Close all parent dropdowns and flyouts after navigation document.querySelectorAll('.horizontal-menu-item.expanded, .horizontal-submenu-item.expanded').forEach((openItem) => { openItem.classList.remove('expanded'); const icon = openItem.querySelector('.icon.toggle-icon'); if (icon) icon.classList.remove('rotated'); }); // Close all flyouts document.querySelectorAll('.horizontal-nested-dropdown').forEach((flyout) => { flyout.style.display = 'none'; flyout.style.opacity = '0'; flyout.style.visibility = 'hidden'; }); }); } return subMenuItem; } /** * Updates the 'active' class for items in the horizontal menu. */ function updateActiveStatesHorizontal(forcedActiveId = null) { // Prioritize forcedActiveId if provided, otherwise use global state. // Ensure the ID is treated as a number for comparison. const targetId = forcedActiveId !== null ? parseInt(String(forcedActiveId)) : parseInt(String(window.activeGalleryId || activeGalleryId)); console.log("[Horizontal Menu Update] Initiated. Target Active ID:", targetId, (forcedActiveId !== null ? `(Forced: ${forcedActiveId})` : "(Global)"), "Type:", typeof targetId); const menuItems = document.querySelectorAll('.horizontal-menu-item, .horizontal-submenu-item'); if (menuItems.length === 0 && !isNaN(targetId)) { // Check if targetId is a valid number console.warn("[Horizontal Menu Update] No horizontal menu items found in DOM to update. Target ID was:", targetId); return; } let activeItemSet = false; menuItems.forEach(item => { const itemIdStr = item.dataset.id; if (!itemIdStr) { // console.warn("[Horizontal Menu Update] Menu item found without a data-id attribute:", item); return; } const itemId = parseInt(itemIdStr); if (itemId === targetId) { if (!item.classList.contains('active')) { item.classList.add('active'); console.log(`[Horizontal Menu Update] ADDED .active to item ID ${itemId} ('${item.textContent.trim()}') using target ID ${targetId}`); } activeItemSet = true; } else { if (item.classList.contains('active')) { item.classList.remove('active'); console.log(`[Horizontal Menu Update] REMOVED .active from item ID ${itemId} ('${item.textContent.trim()}') using target ID ${targetId}`); } } }); if (!isNaN(targetId) && !activeItemSet) { // Check if targetId is a valid number console.warn(`[Horizontal Menu Update] Target Active ID was ${targetId}, but NO matching horizontal menu item was made active. Check data-id attributes and item visibility.`); } const activeSubItem = document.querySelector('.horizontal-submenu-item.active'); if (activeSubItem) { const parentTopItem = activeSubItem.closest('.horizontal-menu-item.has-children'); if (parentTopItem && !parentTopItem.classList.contains('active')) { // parentTopItem.classList.add('active-parent'); // Optional class for styling parent } } console.log("[Horizontal Menu Update] Completed for Target ID:", targetId); } // Listener for closing dropdowns when clicking outside document.addEventListener('click', function(event) { if (currentMenuLayout === 'horizontal') { const openSubmenus = document.querySelectorAll('.horizontal-menu-item.expanded'); let clickedInsideSubmenuOrParent = false; openSubmenus.forEach(submenuContainer => { // Check if click is on the submenu container OR its direct parent menu item if (submenuContainer.contains(event.target) || (submenuContainer.parentElement && submenuContainer.parentElement.contains(event.target) && submenuContainer.parentElement.classList.contains('horizontal-menu-item'))) { clickedInsideSubmenuOrParent = true; } }); // If click is on another top-level item that is NOT expanded, also don't close const targetMenuItem = event.target.closest('.horizontal-menu-item'); if (targetMenuItem && !targetMenuItem.classList.contains('expanded') && targetMenuItem.classList.contains('has-children')) { clickedInsideSubmenuOrParent = true; // Don't close if clicking another parent to open it } if (!clickedInsideSubmenuOrParent) { openSubmenus.forEach(submenuContainer => { submenuContainer.classList.remove('expanded'); }); } } }); document.addEventListener('menuLayoutChanged', function(e) { const newLayout = e.detail.layout; console.log('EVENT: menuLayoutChanged detected. New layout:', newLayout, 'Current isEditing state:', isEditing); // Update the global currentMenuLayout variable if you have one if (typeof currentMenuLayout !== 'undefined') { currentMenuLayout = newLayout; } const sidebar = document.querySelector('.sidebar'); // Ensure sidebar DOM element is selected // Always clear and re-render the appropriate menu structure for the new layout. // This is important for style changes (like font size) that affect dimensions. const galleryTreeContainer = document.getElementById('galleryTree'); const mobileHeaderContent = document.querySelector('.mobile-header-content'); if (galleryTreeContainer) { galleryTreeContainer.innerHTML = ''; // Clear existing tree } if (mobileHeaderContent) { const horizontalMenuContainer = mobileHeaderContent.querySelector('.horizontal-menu-container'); if (horizontalMenuContainer) { horizontalMenuContainer.innerHTML = ''; // Clear only items, not the container itself } } // Render the correct menu structure based on the new layout if (newLayout === 'horizontal') { if (typeof renderHorizontalMenu === 'function') { renderHorizontalMenu(); } // CSS rules will handle sidebar visibility based on .edit-mode-active and .menu-layout-horizontal } else { // 'sidebar' or 'top' if (typeof renderGalleries === 'function') { renderGalleries(); } // CSS rules will handle sidebar visibility } // If currently in edit mode, ensure the UI components for editing are correctly displayed // for the new layout, without fully toggling the mode off and on. if (isEditing && typeof window.updateGlobalEditState === 'function') { console.log('menuLayoutChanged: In edit mode. Ensuring edit UI is consistent for new layout.'); // 1. Forcefully ensure global state and body/sidebar classes are correct for edit mode. // updateGlobalEditState should handle adding .edit-mode-active to body and .editing to sidebar. window.updateGlobalEditState(true); // 2. Re-initialize sortables. It's often safer to destroy existing ones first. if (typeof initializeNestedSortables === 'function') { if (typeof destroyNestedSortables === 'function') { destroyNestedSortables(); } setTimeout(initializeNestedSortables, 100); // Delay for DOM updates } // Also reinitialize for SidebarManager if it exists and handles its own sortables if (window.SidebarManager && typeof window.SidebarManager._reinitializeNestedSortables === 'function') { setTimeout(() => window.SidebarManager._reinitializeNestedSortables(), 150); } // 3. Ensure edit controls (the top bar of buttons in the sidebar) are visible const editControls = document.querySelector('.edit-controls'); if (editControls) { editControls.style.display = 'flex'; // Or your default display type for these controls editControls.classList.add('visible'); } // 4. If the layout is now horizontal, the sidebar (for editing) should be visible. // The CSS rule `body.menu-layout-horizontal.edit-mode-active .sidebar { display: block !important; }` // should handle this. This log is for confirmation. if (newLayout === 'horizontal' && sidebar) { console.log('menuLayoutChanged: Horizontal layout in edit mode, sidebar should be visible via CSS.'); } // 5. If PageManager is active and a page is loaded, ensure its edit mode is also set. if (window.PageManager && typeof window.PageManager.setEditMode === 'function' && window.PageManager.getCurrentPageId && window.PageManager.getCurrentPageId()) { window.PageManager.setEditMode(true); } } else if (!isEditing) { // If not in edit mode, CSS should handle the view state. // This block can be used for any explicit view mode adjustments if CSS isn't sufficient. console.log('menuLayoutChanged: Not in edit mode. CSS will handle view state.'); } // Update mobile title and close mobile menu if open, regardless of edit mode if (typeof updateMobileTitle === 'function') { updateMobileTitle(); } if (typeof closeMobileMenu === 'function') { closeMobileMenu(); } }); // Make sure getFirstSidebarElementForHeader is available if called from other scripts if (typeof window.getFirstSidebarElementForHeader === 'undefined') { window.getFirstSidebarElementForHeader = getFirstSidebarElementForHeader; } // Make sure getFirstSocialElementForHeader is available if called from other scripts if (typeof window.getFirstSocialElementForHeader === 'undefined') { window.getFirstSocialElementForHeader = getFirstSocialElementForHeader; } // Make sure renderSocialIcons is available if called from other scripts if (typeof window.renderSocialIcons === 'undefined') { window.renderSocialIcons = renderSocialIcons; } if (typeof window.renderHorizontalMenu === 'undefined') { window.renderHorizontalMenu = renderHorizontalMenu; } if (typeof window.createHorizontalMenuItem === 'undefined') { window.createHorizontalMenuItem = createHorizontalMenuItem; } if (typeof window.createHorizontalSubMenuItem === 'undefined') { window.createHorizontalSubMenuItem = createHorizontalSubMenuItem; } if (typeof window.updateActiveStatesHorizontal === 'undefined') { window.updateActiveStatesHorizontal = updateActiveStatesHorizontal; } // Ensure toggleEditMode is globally available if it's the primary one. if (typeof window.toggleEditMode === 'undefined' || window.toggleEditMode.toString().length < 100) { // Heuristic to check if it's a placeholder window.toggleEditMode = toggleEditMode; } // Helper function to toggle edit controls visibility function toggleEditControlsVisibility(show) { const editControls = document.querySelector('.edit-controls'); if (editControls) { if (show) { editControls.classList.add('visible'); editControls.style.display = 'flex'; } else { editControls.classList.remove('visible'); editControls.style.display = 'none'; } } } document.addEventListener('keydown', function(e) { if (e.key === '+' || e.keyCode === 187 && e.shiftKey) { // Check if user is already logged in and is admin const token = window.didToken || localStorage.getItem('hydra_auth_token'); const isAdmin = window.isAdmin || localStorage.getItem('hydra_is_admin') === 'true'; if (token && isAdmin) { // User is already logged in - toggle edit mode instead of showing login prompt console.log('User already logged in - toggling edit mode'); if (typeof window.toggleEditMode === 'function') { window.toggleEditMode(); } else if (typeof toggleEditMode === 'function') { toggleEditMode(); } else { console.warn('toggleEditMode function not found'); } } else { // User is not logged in - show login prompt startOTPLogin(); } e.preventDefault(); } }); // Email OTP Authentication with Enhanced Debugging // Add this function to the client-side code // Improved client-side backup function with better token handling async function createLoginBackup() { try { console.log('Attempting to create login backup...'); // Get authentication info from multiple possible sources let token = window.didToken || localStorage.getItem('hydra_auth_token'); const email = localStorage.getItem('hydra_auth_email') || (window.userMetadata ? window.userMetadata.email : '') || ''; // Check if we have authentication if (!token) { console.error('No auth token available for backup'); return; } if (!email) { console.error('No email available for backup - make sure it was stored during login'); return; } console.log(`Creating backup with token (${token.length} chars) for ${email}`); // Format the token correctly - VERY IMPORTANT // If it's already a hydra token (starts with hydra:), use it as is // Otherwise, add the hydra: prefix const formattedToken = token.startsWith('hydra:') ? token : `hydra:${token}`; console.log(`Using formatted token: ${formattedToken.substring(0, 15)}...`); // Get the API URL with preview handling if needed const backupUrl = typeof getApiUrl === 'function' ? getApiUrl('/api/create-backup') : '/api/create-backup'; console.log(`Sending backup request to: ${backupUrl}`); // IMPORTANT: Make sure to add the Bearer prefix to the Authorization header const response = await fetch(backupUrl, { method: 'POST', headers: { 'Authorization': `Bearer ${formattedToken}`, 'Content-Type': 'application/json', 'X-User-Email': email, // Include email as fallback 'X-API-Request': 'true' // Mark as API request } }); // Handle response if (!response.ok) { const errorText = await response.text(); let errorData; try { // Try to parse as JSON errorData = JSON.parse(errorText); console.error('Backup failed:', errorData.error, errorData); } catch { // Not JSON, log raw text console.error('Backup failed with status', response.status, errorText); } return; } // Parse successful response const result = await response.json(); console.log(`✅ Backup created successfully: ${result.backup}`); } catch (error) { console.error('Error in backup process:', error); } } // Initialize Magic with detailed logging function initMagic() { try { // If Magic is already initialized, return it if (window.magic) { return window.magic; } console.log('Initializing Magic SDK...'); // Initialize with minimal options const magic = new Magic('pk_live_E0B68BA6DCA8C75F', { testMode: false }); // Store reference globally window.magic = magic; console.log('Magic SDK initialized successfully'); return magic; } catch (error) { console.error('Error initializing Magic:', error); throw error; } } // Start OTP login flow with proper event handling async function startOTPLogin() { try { // Initialize Magic const magic = initMagic(); // Get email from user const email = prompt('Please enter your email:'); if (!email) return; // User cancelled console.log('Starting Email OTP login for:', email); showLoadingOverlay('Sending verification code...'); // Check if Magic's event enum types are available - for newer Magic SDK versions // If not available, fall back to string-based events const useEnums = typeof LoginWithEmailOTPEventOnReceived !== 'undefined' && typeof LoginWithEmailOTPEventEmit !== 'undefined'; console.log('Using enum-based events:', useEnums); // Create a timeout handler let emailSentTimeout = setTimeout(() => { console.log('No email-otp-sent event received, using fallback UI'); hideLoadingOverlay(); showOTPInputOverlay(email); }, 10000); // Create OTP login handle with showUI: false const handle = magic.auth.loginWithEmailOTP({ email, showUI: false, deviceCheckUI: false }); // Track authentication state window.pendingAuth = { email, handle }; // Listen for events - using both enum-based and string-based event handling console.log('Setting up OTP event handlers...'); // Email OTP sent event if (useEnums) { handle.on(LoginWithEmailOTPEventOnReceived.EmailOTPSent, () => { console.log('EVENT: EmailOTPSent (enum) - Email was sent successfully'); clearTimeout(emailSentTimeout); hideLoadingOverlay(); showOTPInputOverlay(email); }); } // Also listen for string-based event (for backward compatibility) handle.on('email-otp-sent', () => { console.log('EVENT: email-otp-sent - Email was sent successfully'); clearTimeout(emailSentTimeout); hideLoadingOverlay(); showOTPInputOverlay(email); }); // Invalid OTP event if (useEnums) { handle.on(LoginWithEmailOTPEventOnReceived.InvalidEmailOtp, () => { console.log('EVENT: InvalidEmailOtp (enum) - Invalid code entered'); showErrorMessage('Invalid verification code. Please try again.'); updateOTPInputForRetry(); }); } // Also listen for string-based event handle.on('invalid-email-otp', () => { console.log('EVENT: invalid-email-otp - Invalid code entered'); showErrorMessage('Invalid verification code. Please try again.'); updateOTPInputForRetry(); }); function handleLoginSuccess(result) { console.log('OTP login successful!'); hideOTPInputOverlay(); hideLoadingOverlay(); // Store the didToken window.didToken = result; console.log('DID Token received:', result ? (result.substring(0, 20) + '...') : 'none'); // Set isAuthenticated globally window.isAuthenticated = true; // Get user metadata magic.user.getInfo().then(userInfo => { window.userMetadata = userInfo; console.log('User info retrieved:', userInfo); // Call checkAdminStatus with the token return checkAdminStatus(result); }).then(() => { console.log('Admin status check completed'); // Show success message showSuccessMessage('Login successful!'); // IMPROVED UI UPDATE APPROACH - Use multiple techniques for redundancy // 1. Add state classes to body document.body.classList.add('hydra-authenticated'); if (window.isAdmin) { document.body.classList.add('hydra-admin'); } // 2. Direct DOM manipulation const logoutButton = document.getElementById('logoutButton'); const editButton = document.getElementById('editButton'); // Create new buttons if they don't exist if (!logoutButton) { console.log('Creating logout button'); const newLogoutButton = document.createElement('button'); newLogoutButton.id = 'logoutButton'; newLogoutButton.className = 'btn'; newLogoutButton.textContent = 'Logout'; newLogoutButton.onclick = logout; newLogoutButton.style.display = 'block'; // Add to sidebar header const sidebarHeader = document.querySelector('.sidebar-header'); if (sidebarHeader) { sidebarHeader.appendChild(newLogoutButton); } else { document.body.appendChild(newLogoutButton); } } else { // Force display of existing button logoutButton.style.cssText = 'display: block !important; visibility: visible !important; opacity: 1 !important;'; } if (!editButton && window.isAdmin) { console.log('Creating edit button'); const newEditButton = document.createElement('button'); newEditButton.id = 'editButton'; newEditButton.className = 'btn btn-primary'; newEditButton.innerHTML = ` Edit `; newEditButton.onclick = toggleEditMode; newEditButton.style.display = 'block'; // Add to sidebar header const sidebarHeader = document.querySelector('.sidebar-header'); if (sidebarHeader) { sidebarHeader.appendChild(newEditButton); } else { document.body.appendChild(newEditButton); } } else if (editButton && window.isAdmin) { // Force display of existing button editButton.style.cssText = 'display: block !important; visibility: visible !important; opacity: 1 !important;'; } // 3. Dispatch event for other listeners document.dispatchEvent(new CustomEvent('login-complete', { detail: { isAdmin: window.isAdmin, email: window.userMetadata ? window.userMetadata.email : null } })); // Clear pending auth window.pendingAuth = null; }).catch(error => { console.error('Error in login success handling:', error); showErrorMessage('Error completing login: ' + error.message); }); } // Completion event handle.on('done', async (result) => { console.log('OTP login successful!'); hideOTPInputOverlay(); hideLoadingOverlay(); handleLoginSuccess(); // Store the didToken window.didToken = result; console.log('DID Token received:', result ? (result.substring(0, 20) + '...') : 'none'); // Set isAuthenticated globally window.isAuthenticated = true; document.body.classList.add('hydra-authenticated'); // Get user metadata try { const userInfo = await magic.user.getInfo(); window.userMetadata = userInfo; console.log('User info retrieved:', userInfo); } catch (error) { console.error('Error getting user info:', error); } // Call checkAdminStatus with the token try { if (typeof checkAdminStatus === 'function') { await checkAdminStatus(result); } else { console.warn('checkAdminStatus function not available'); } } catch (error) { console.error('Error checking admin status:', error); } // Show success message showSuccessMessage('Login successful!'); // Get the ID token (JWT) for API calls - this contains user info try { const idToken = await magic.user.getIdToken(); console.log('ID Token received, length:', idToken ? idToken.length : 0); console.log('ID Token format check - contains dots:', idToken ? idToken.includes('.') : false); console.log('ID Token prefix:', idToken ? idToken.substring(0, 20) + '...' : 'none'); // Check if this is actually a JWT token (should contain dots) if (idToken && idToken.includes('.')) { console.log('Got proper JWT token from Magic.link'); localStorage.setItem('hydra_auth_token', idToken); } else { console.log('ID token is not JWT format, trying to get user info...'); // If not JWT, get user metadata and create a simple token const metadata = await magic.user.getInfo(); console.log('User metadata:', metadata); if (metadata && metadata.email) { // Create a simple token with user email const userToken = { email: metadata.email, iat: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) + (24 * 60 * 60) }; // Create a proper JWT-like structure: header.payload.signature const header = btoa(JSON.stringify({ typ: 'JWT', alg: 'none' })); const payload = btoa(JSON.stringify(userToken)); const signature = 'nosignature'; const jwtToken = header + '.' + payload + '.' + signature; console.log('Created JWT-like token with email:', metadata.email); localStorage.setItem('hydra_auth_token', jwtToken); localStorage.setItem('hydra_auth_email', metadata.email); } else { throw new Error('Could not get user metadata'); } } // Also store DID token separately if needed localStorage.setItem('hydra_did_token', result); } catch (error) { console.error('Error getting ID token:', error); // Fallback to DID token if ID token fails localStorage.setItem('hydra_auth_token', result); } // Update UI directly with classes document.body.classList.add('hydra-authenticated'); if (isAdmin) { document.body.classList.add('hydra-admin'); } // Clear pending auth window.pendingAuth = null; // Run existing checkAuth if available if (typeof checkAuth === 'function') { console.log('Running existing checkAuth function'); setTimeout(checkAuth, 500); } }); // Error event handle.on('error', (error) => { console.error('EVENT: error - OTP login error:', error); hideLoadingOverlay(); hideOTPInputOverlay(); showErrorMessage('Login failed: ' + (error.message || 'Unknown error')); // Clear timeout if still active clearTimeout(emailSentTimeout); // Clean up window.pendingAuth = null; }); console.log('OTP event handlers set up successfully'); } catch (error) { console.error('Error starting OTP login:', error); hideLoadingOverlay(); hideOTPInputOverlay(); showErrorMessage('Error starting login: ' + error.message); } } // Submit OTP code with support for enum-based events function submitOTP(code) { try { // Validate code input if (!code || code.trim() === '') { showErrorMessage('Please enter the verification code.'); return; } // Check if we have a pending authentication if (!window.pendingAuth || !window.pendingAuth.handle) { showErrorMessage('No active login session. Please try again.'); hideOTPInputOverlay(); return; } console.log('Submitting OTP code...'); showLoadingOverlay('Verifying code...'); // Check if enum types are available const useEnums = typeof LoginWithEmailOTPEventEmit !== 'undefined'; // Verify the OTP code if (useEnums) { window.pendingAuth.handle.emit(LoginWithEmailOTPEventEmit.VerifyEmailOtp, code); } else { window.pendingAuth.handle.emit('verify-email-otp', code); } } catch (error) { console.error('Error submitting OTP:', error); hideLoadingOverlay(); showErrorMessage('Error verifying code: ' + error.message); } } // Cancel OTP login with support for enum-based events function cancelOTPLogin() { try { // Check if we have a pending authentication if (window.pendingAuth && window.pendingAuth.handle) { // Check if enum types are available const useEnums = typeof LoginWithEmailOTPEventEmit !== 'undefined'; // Emit cancel event if (useEnums) { window.pendingAuth.handle.emit(LoginWithEmailOTPEventEmit.Cancel); } else { window.pendingAuth.handle.emit('cancel'); } console.log('OTP login cancelled'); } // Clean up window.pendingAuth = null; // Hide any overlays hideOTPInputOverlay(); hideLoadingOverlay(); } catch (error) { console.error('Error cancelling OTP login:', error); } } // Update OTP input for retry function updateOTPInputForRetry() { const otpInput = document.getElementById('otp-input'); if (otpInput) { otpInput.value = ''; otpInput.focus(); // Add a shake animation for visual feedback otpInput.classList.add('shake'); // Remove the animation class after it completes setTimeout(() => { otpInput.classList.remove('shake'); }, 500); } // Hide loading overlay if it's visible hideLoadingOverlay(); } // Show OTP input overlay - improved version function showOTPInputOverlay(email) { // Remove existing overlay if any hideOTPInputOverlay(); // Create overlay const overlay = document.createElement('div'); overlay.id = 'otp-input-overlay'; overlay.style.position = 'fixed'; overlay.style.top = '0'; overlay.style.left = '0'; overlay.style.width = '100%'; overlay.style.height = '100%'; overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'; overlay.style.display = 'flex'; overlay.style.alignItems = 'center'; overlay.style.justifyContent = 'center'; overlay.style.zIndex = '10000'; // Create content box const content = document.createElement('div'); content.style.backgroundColor = 'white'; content.style.padding = '30px'; content.style.borderRadius = '0px'; content.style.maxWidth = '400px'; content.style.width = '90%'; content.style.textAlign = 'center'; content.style.boxShadow = '0 4px 20px rgba(0,0,0,0.3)'; // Add logo const logo = document.createElement('img'); logo.src = 'https://cdn.neonsky.app/neon-sky-logo.png'; logo.alt = 'Neon Sky Logo'; logo.style.width = '250px'; logo.style.marginBottom = '15px'; // Add title const title = document.createElement('h3'); title.textContent = ''; title.style.margin = '0 0 15px 0'; title.style.fontSize = '20px'; // Add description const description = document.createElement('p'); description.innerHTML = `We've sent a verification code to ${email}.
Please check your email and enter the code below:`; description.style.marginBottom = '20px'; description.style.fontSize = '14px'; description.style.lineHeight = '1.4'; description.style.color = '#555'; // Append elements content.appendChild(logo); // Add logo at the top content.appendChild(title); content.appendChild(description); overlay.appendChild(content); document.body.appendChild(overlay); // Create form const form = document.createElement('form'); form.onsubmit = function(e) { e.preventDefault(); const otpInput = document.getElementById('otp-input'); if (otpInput && otpInput.value) { submitOTP(otpInput.value); } }; // Add animation styles if (!document.getElementById('otp-animation-styles')) { const style = document.createElement('style'); style.id = 'otp-animation-styles'; style.textContent = ` @keyframes shake { 0%, 100% { transform: translateX(0); } 10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); } 20%, 40%, 60%, 80% { transform: translateX(5px); } } .shake { animation: shake 0.5s; } `; document.head.appendChild(style); } // Create input const input = document.createElement('input'); input.id = 'otp-input'; input.type = 'text'; input.placeholder = 'Enter code'; input.pattern = '[0-9]*'; // Numbers only input.inputMode = 'numeric'; // Show numeric keyboard on mobile input.autocomplete = 'one-time-code'; // For OTP autocomplete input.style.width = '100%'; input.style.padding = '12px'; input.style.fontSize = '20px'; input.style.textAlign = 'center'; input.style.letterSpacing = '4px'; input.style.fontWeight = 'bold'; input.style.border = '2px solid #ddd'; input.style.borderRadius = '0px'; input.style.marginBottom = '20px'; input.style.boxSizing = 'border-box'; form.appendChild(input); // Create button container const buttonContainer = document.createElement('div'); buttonContainer.style.display = 'flex'; buttonContainer.style.justifyContent = 'space-between'; buttonContainer.style.gap = '10px'; // Create verify button const verifyButton = document.createElement('button'); verifyButton.type = 'submit'; verifyButton.textContent = 'Verify'; verifyButton.style.flex = '1'; verifyButton.style.padding = '10px'; verifyButton.style.backgroundColor = '#4682B4'; verifyButton.style.color = 'white'; verifyButton.style.border = 'none'; verifyButton.style.borderRadius = '0px'; verifyButton.style.fontSize = '16px'; verifyButton.style.cursor = 'pointer'; buttonContainer.appendChild(verifyButton); // Create cancel button const cancelButton = document.createElement('button'); cancelButton.type = 'button'; cancelButton.textContent = 'Cancel'; cancelButton.style.flex = '1'; cancelButton.style.padding = '10px'; cancelButton.style.backgroundColor = '#f1f1f1'; cancelButton.style.color = '#333'; cancelButton.style.border = 'none'; cancelButton.style.borderRadius = '0px'; cancelButton.style.fontSize = '16px'; cancelButton.style.cursor = 'pointer'; cancelButton.onclick = function() { cancelOTPLogin(); }; buttonContainer.appendChild(cancelButton); form.appendChild(buttonContainer); // Add resend option const resendContainer = document.createElement('div'); resendContainer.style.marginTop = '15px'; resendContainer.style.fontSize = '14px'; resendContainer.style.color = '#666'; const resendText = document.createElement('span'); resendText.textContent = "Didn't receive the code? "; const resendLink = document.createElement('a'); resendLink.textContent = "Resend"; resendLink.href = "#"; resendLink.style.color = '#4682B4'; resendLink.style.textDecoration = 'none'; resendLink.onclick = function(e) { e.preventDefault(); cancelOTPLogin(); setTimeout(() => { startOTPLogin(); }, 500); }; resendContainer.appendChild(resendText); resendContainer.appendChild(resendLink); form.appendChild(resendContainer); content.appendChild(form); overlay.appendChild(content); document.body.appendChild(overlay); // Focus the input setTimeout(() => { input.focus(); }, 100); } // Hide OTP input overlay function hideOTPInputOverlay() { const overlay = document.getElementById('otp-input-overlay'); if (overlay && overlay.parentNode) { overlay.parentNode.removeChild(overlay); } } // Utility function to show loading overlay function showLoadingOverlay(message = 'Loading...') { // Remove any existing overlay first hideLoadingOverlay(); const overlay = document.createElement('div'); overlay.id = 'loading-overlay'; overlay.style.position = 'fixed'; overlay.style.top = '0'; overlay.style.left = '0'; overlay.style.width = '100%'; overlay.style.height = '100%'; overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'; overlay.style.display = 'flex'; overlay.style.alignItems = 'center'; overlay.style.justifyContent = 'center'; overlay.style.zIndex = '10000'; const content = document.createElement('div'); content.style.backgroundColor = 'white'; content.style.padding = '30px'; content.style.borderRadius = '0px'; content.style.textAlign = 'center'; content.style.maxWidth = '400px'; content.style.width = '90%'; // Add SVG loader instead of spinner const loaderContainer = document.createElement('div'); loaderContainer.style.margin = '0 auto 20px auto'; loaderContainer.style.color = '#444444'; // Dark grey color // Add SVG animation loaderContainer.innerHTML = ` `; content.appendChild(loaderContainer); // Add message const messageEl = document.createElement('p'); messageEl.textContent = message; messageEl.style.margin = '0'; messageEl.style.fontSize = '16px'; content.appendChild(messageEl); overlay.appendChild(content); document.body.appendChild(overlay); } // Utility function to hide loading overlay function hideLoadingOverlay() { const overlay = document.getElementById('loading-overlay'); if (overlay && overlay.parentNode) { overlay.parentNode.removeChild(overlay); } } // Utility function to show success message function showSuccessMessage(message, duration = 3000) { const toast = document.createElement('div'); toast.id = 'success-toast'; toast.style.position = 'fixed'; toast.style.top = '20px'; toast.style.left = '50%'; toast.style.transform = 'translateX(-50%)'; toast.style.backgroundColor = '#4682B4'; toast.style.color = 'white'; toast.style.padding = '12px 24px'; toast.style.borderRadius = '0px'; toast.style.boxShadow = '0 4px 12px rgba(0,0,0,0.2)'; toast.style.zIndex = '10000'; toast.style.fontSize = '16px'; toast.textContent = message; document.body.appendChild(toast); // Remove after duration setTimeout(() => { if (toast.parentNode) { toast.parentNode.removeChild(toast); } }, duration); } // Utility function to show error message function showErrorMessage(message, duration = 5000) { const toast = document.createElement('div'); toast.id = 'error-toast'; toast.style.position = 'fixed'; toast.style.top = '20px'; toast.style.left = '50%'; toast.style.transform = 'translateX(-50%)'; toast.style.backgroundColor = '#e74c3c'; toast.style.color = 'white'; toast.style.padding = '12px 24px'; toast.style.borderRadius = '0px'; toast.style.boxShadow = '0 4px 12px rgba(0,0,0,0.2)'; toast.style.zIndex = '10000'; toast.style.fontSize = '16px'; toast.textContent = message; document.body.appendChild(toast); // Remove after duration setTimeout(() => { if (toast.parentNode) { toast.parentNode.removeChild(toast); } }, duration); } // UPDATED: Check authentication status with class-based approach async function checkAuth() { try { if (!magic) { console.log('Magic not initialized, skipping auth check'); return; } // Prevent multiple simultaneous auth checks if (window._checkingAuth) return; window._checkingAuth = true; console.log('Checking auth status...'); isAuthenticated = await magic.user.isLoggedIn(); console.log('isLoggedIn check:', isAuthenticated); if (isAuthenticated) { // Add the authenticated class document.body.classList.add('hydra-authenticated'); // Get user metadata userMetadata = await magic.user.getInfo(); console.log('User info:', userMetadata); // Get ID token for API calls (JWT format) didToken = await magic.user.getIdToken(); console.log('ID token acquired, length:', didToken.length); // Store token in localStorage localStorage.setItem('hydra_auth_token', didToken); // Check if user is admin await checkAdminStatus(didToken); } else { resetAuthUI(); } // Release lock window._checkingAuth = false; } catch (error) { console.error('Error checking authentication:', error); window._checkingAuth = false; resetAuthUI(); } } async function checkAdminStatus(token) { try { console.log('Checking admin status...'); // Add user email to headers if available let userEmail = null; if (window.userMetadata && window.userMetadata.email) { userEmail = window.userMetadata.email; console.log('Using email from userMetadata:', userEmail); } else { userEmail = localStorage.getItem('hydra_auth_email'); // Fallback to localStorage if (userEmail) console.log('Using email from localStorage:', userEmail); } if (!token) { console.log('No token provided to checkAdminStatus'); return false; // Indicate failure } // Format the token correctly for the API call let formattedToken = token; // Ensure Bearer prefix is added for Magic tokens, or use hydra: prefix if (!token.startsWith('hydra:') && !token.startsWith('Bearer ')) { formattedToken = `Bearer ${token}`; // Assume Magic/JWT needs Bearer } else if (token.startsWith('hydra:')) { formattedToken = `Bearer ${token}`; // API expects Bearer even for hydra tokens } // If it already starts with Bearer, use as is // Get the API URL, handling preview mode const apiUrl = typeof getApiUrl === 'function' ? getApiUrl('/api/check-admin') : '/api/check-admin'; console.log('Sending admin check request to:', apiUrl); // Create request headers const headers = { 'Authorization': formattedToken, 'X-Hydra-Request': 'true' // Indicate this might be part of Hydra flow }; if (userEmail) { headers['X-User-Email'] = userEmail; } console.log('Admin check headers:', { 'Authorization': `${formattedToken.substring(0, 20)}...`, 'X-User-Email': userEmail || 'Not available' }); // Add timeout to the fetch request const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout let data; try { const response = await fetch(apiUrl, { method: 'GET', headers: headers, signal: controller.signal }); clearTimeout(timeoutId); // Clear timeout if fetch succeeds console.log('Admin check response status:', response.status); // Get the response as text first for better debugging const responseText = await response.text(); console.log('Admin check response length:', responseText.length); console.log('Admin check response text (first 100 chars):', responseText.substring(0, 100) + '...'); try { data = JSON.parse(responseText); console.log('Admin check data:', data); } catch (parseError) { console.error('Error parsing admin check response:', parseError); // Handle cases where the response might not be JSON (e.g., HTML error page) // For preview URLs, let's still assume admin to allow editing flow if (window.location.hostname === 'preview.neonsky.app') { console.log('Preview URL detected, assuming admin status due to non-JSON response'); isAdmin = true; document.body.classList.add('hydra-admin'); localStorage.setItem('hydra_is_admin', 'true'); forceShowEditUI(); // Attempt to show UI return true; // Indicate potential success for preview } throw new Error(`Failed to parse admin check response: ${responseText.substring(0, 100)}...`); } if (!response.ok) { // Throw an error with details from the parsed JSON if available throw new Error(`Admin check failed: ${response.status} - ${data?.error || responseText.substring(0, 50)}`); } // Process the admin status isAdmin = data.isAdmin; window.isAdmin = isAdmin; // Update global flag // Update classes based on admin status if (isAdmin) { document.body.classList.add('hydra-admin'); localStorage.setItem('hydra_is_admin', 'true'); // Store email if we got it from the server response if (data.email) { localStorage.setItem('hydra_auth_email', data.email); } else if (userEmail) { localStorage.setItem('hydra_auth_email', userEmail); // Store the email we used } } else { document.body.classList.remove('hydra-admin'); localStorage.removeItem('hydra_is_admin'); localStorage.removeItem('hydra_auth_email'); // Clear email if not admin } // Update siteId if provided if (data.siteId) { siteId = data.siteId; window.siteId = siteId; if (window.Parameters) window.Parameters.siteId = siteId; } // Store new token if provided (Hydra token) if (data.hydraToken) { console.log('Received hydra token:', data.hydraToken.substring(0, 10) + '...'); // Store token globally and in localStorage using TokenManager if available if (window.TokenManager && typeof window.TokenManager.storeToken === 'function') { window.TokenManager.storeToken(data.hydraToken, data.email || userEmail || ''); } else { // Fallback storage const cleanToken = data.hydraToken.startsWith('hydra:') ? data.hydraToken.substring(6) : data.hydraToken; didToken = cleanToken; localStorage.setItem('hydra_auth_token', cleanToken); if (data.email || userEmail) { localStorage.setItem('hydra_auth_email', data.email || userEmail); } } console.log('Stored credentials in localStorage'); } // Update UI based on admin status if (isAdmin) { console.log('Admin status confirmed, showing edit UI'); forceShowEditUI(); // Ensure UI elements are visible // --- MODIFICATION START --- // If layout is horizontal, re-render the menu to show controls let currentLayout = 'sidebar'; // Default if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) { currentLayout = window.MenuStyleCustomizer.settings.menuLayout || 'sidebar'; } if (currentLayout === 'horizontal') { console.log('checkAdminStatus: Horizontal layout detected, re-rendering horizontal menu.'); if (typeof renderHorizontalMenu === 'function') { renderHorizontalMenu(); } else { console.warn("checkAdminStatus: renderHorizontalMenu function not found."); } } // --- MODIFICATION END --- createLoginBackup(); // Create backup after successful login/admin check } else { console.log('User is not admin, resetting UI.'); resetAuthUI(); // Ensure non-admin UI state } return isAdmin; // Return the admin status } catch (fetchError) { clearTimeout(timeoutId); // Clear timeout on error as well console.error('Fetch error during admin check:', fetchError); // For preview URLs, still allow editing as a fallback if (window.location.hostname === 'preview.neonsky.app') { console.log('Preview URL + fetch error, still enabling editing'); isAdmin = true; window.isAdmin = true; document.body.classList.add('hydra-admin'); localStorage.setItem('hydra_is_admin', 'true'); forceShowEditUI(); return true; // Indicate potential success for preview } // For non-preview, throw the error to indicate failure throw fetchError; } } catch (error) { console.error('Error in checkAdminStatus:', error); // Don't show alert here, let the calling function handle UI feedback // Reset UI to non-admin state on error resetAuthUI(); return false; // Indicate failure } } // UPDATED: Force showing edit UI with class-based approach function forceShowEditUI() { console.log('Forcing admin/auth state for UI'); // Ensure global state flags are set isAuthenticated = true; isAdmin = true; window.isAuthenticated = true; window.isAdmin = true; // Add admin/auth classes to body - CSS will handle button visibility document.body.classList.add('hydra-authenticated', 'hydra-admin'); // Store admin status in localStorage localStorage.setItem('hydra_is_admin', 'true'); // Show edit controls container ONLY if already in edit mode if (window.isInEditMode && window.isInEditMode()) { const editControls = document.querySelector('.edit-controls'); if (editControls) { editControls.classList.add('visible'); editControls.style.display = 'flex'; // Make sure container is flex } // Ensure controls inside items are visible if already editing document.querySelectorAll('.controls, .element-controls, .edit-btn, .delete-btn, .visibility-toggle').forEach(el => { if (el.closest('.sidebar.editing') || el.closest('.page-container.editing')) { el.style.display = 'flex'; } }); // Show logout button in edit controls when in edit mode const logoutButton = document.getElementById('logoutButton'); if (logoutButton) { logoutButton.style.display = 'flex'; logoutButton.classList.add('visible'); } } else { // Ensure edit controls container is hidden if not in edit mode const editControls = document.querySelector('.edit-controls'); if (editControls) { editControls.classList.remove('visible'); editControls.style.display = 'none'; } // Hide logout button when not in edit mode const logoutButton = document.getElementById('logoutButton'); if (logoutButton) { logoutButton.style.display = 'none'; logoutButton.classList.remove('visible'); } } console.log('Admin/Auth classes set. CSS should handle button visibility.'); } // UPDATED: Reset auth UI with class-based approach function resetAuthUI() { document.body.classList.remove('hydra-authenticated'); document.body.classList.remove('hydra-admin'); const logoutButton = document.getElementById('logoutButton'); if (logoutButton) { logoutButton.style.display = 'none'; logoutButton.classList.remove('visible'); } const editButton = document.getElementById('editButton'); if (editButton) { editButton.style.display = 'none'; editButton.classList.remove('visible'); } // Remove admin status from localStorage localStorage.removeItem('hydra_is_admin'); localStorage.removeItem('hydra_auth_token'); if (isEditing) { toggleEditMode(); // Exit edit mode if active } } // Debug function - add to your code during testing function debugUIClasses() { console.log('Current body classes:', document.body.className); console.log('hydra-initialized:', document.body.classList.contains('hydra-initialized')); console.log('hydra-authenticated:', document.body.classList.contains('hydra-authenticated')); console.log('hydra-admin:', document.body.classList.contains('hydra-admin')); console.log('edit-mode-active:', document.body.classList.contains('edit-mode-active')); } async function logout() { console.log('Logout initiated'); try { // Show loading indication (optional) showLoadingOverlay('Logging out...'); // 1. Properly logout from Magic SDK if (window.magic) { try { await window.magic.user.logout(); console.log('Magic SDK logout successful'); } catch (magicError) { console.error('Error during Magic logout:', magicError); // Continue with other logout steps even if Magic SDK logout fails } } // 2. Reset all authentication state variables window.isAuthenticated = false; window.isAdmin = false; window.userMetadata = null; window.didToken = null; // 3. Clear all authentication data from localStorage localStorage.removeItem('hydra_is_admin'); localStorage.removeItem('hydra_auth_token'); localStorage.removeItem('hydra_auth_email'); // 4. Reset all UI state - remove authentication classes from body document.body.classList.remove('hydra-authenticated'); document.body.classList.remove('hydra-admin'); // 5. If in edit mode, exit it first if (document.body.classList.contains('edit-mode-active')) { console.log('Exiting edit mode before logout'); // If toggleEditMode exists, call it to exit edit mode if (typeof toggleEditMode === 'function') { try { toggleEditMode(); } catch (editError) { console.error('Error exiting edit mode:', editError); } } // Ensure edit mode class is removed regardless document.body.classList.remove('edit-mode-active'); } // 6. Ensure all edit UI elements are hidden // Hide edit and logout buttons const editButton = document.getElementById('editButton'); if (editButton) { editButton.classList.remove('visible'); editButton.style.display = 'none'; } const logoutButton = document.getElementById('logoutButton'); if (logoutButton) { logoutButton.classList.remove('visible'); logoutButton.style.display = 'none'; } // Hide edit controls const editControls = document.querySelector('.edit-controls'); if (editControls) { editControls.style.display = 'none'; editControls.classList.remove('visible'); } // Hide sidebar header if it should be hidden when logged out const sidebarHeader = document.querySelector('.sidebar-header'); if (sidebarHeader) { sidebarHeader.style.display = 'none'; } // Hide all element controls document.querySelectorAll('.controls, .element-controls, .edit-btn, .delete-btn, .visibility-toggle').forEach(el => { el.style.display = 'none'; }); // 7. Reset editor state if applicable if (window.Parameters) { window.Parameters.isInEditor = false; } // Hide any editing forms that might be open const forms = document.querySelectorAll('.add-form, .edit-form, .element-edit-form, #menuStyleEditor, #metadataEditor, #importClassicForm, #sidebarElementForm'); forms.forEach(form => { form.style.display = 'none'; if (form.classList.contains('visible')) { form.classList.remove('visible'); } }); // Remove editing class from sidebar const sidebar = document.querySelector('.sidebar'); if (sidebar) { sidebar.classList.remove('editing'); } // 8. Clear any page-specific state if (window.PageManager && typeof window.PageManager.clearCurrentPage === 'function') { window.PageManager.clearCurrentPage(); } // 9. Notify any components that need to know about logout document.dispatchEvent(new CustomEvent('user-logout', { detail: { timestamp: Date.now() } })); // 10. Show success message hideLoadingOverlay(); showSuccessMessage('Successfully logged out'); console.log('Logout complete'); // 11. Optional: Refresh the page after a short delay for a clean state // Uncomment the following lines if you want the page to refresh after logout /* setTimeout(() => { window.location.reload(); }, 1500); */ } catch (error) { console.error('Error during logout process:', error); hideLoadingOverlay(); showErrorMessage('Error during logout. Please refresh the page.'); } } // Ensure the logout button is properly connected to the logout function document.addEventListener('DOMContentLoaded', function() { const logoutButton = document.getElementById('logoutButton'); if (logoutButton) { // Remove any existing event listeners to avoid duplicates const newButton = logoutButton.cloneNode(true); logoutButton.parentNode.replaceChild(newButton, logoutButton); // Add fresh event listener newButton.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); logout(); }); } }); function stopAutoAdvanceTimer() { if (window.currentAutoAdvanceTimerId) { clearTimeout(window.currentAutoAdvanceTimerId); window.currentAutoAdvanceTimerId = null; console.log('Auto-advance timer stopped.'); } } window.stopAutoAdvanceTimer = stopAutoAdvanceTimer; function toggleEditMode() { // Prevent multiple simultaneous calls if (window._editModeToggleInProgress) { console.log('Edit mode toggle already in progress, skipping'); return; } window._editModeToggleInProgress = true; console.log("toggleEditMode called. Current window.isEditing:", window.isEditing, "Current window.isInEditMode():", window.isInEditMode ? window.isInEditMode() : 'undefined'); stopAutoAdvanceTimer(); // Assumes this function exists and stops any slideshow/auto-advance // Authentication and Authorization Checks if (!window.isAuthenticated && !(localStorage.getItem('hydra_is_admin') === 'true')) { alert('You must be logged in to edit'); window._editModeToggleInProgress = false; return; } if (localStorage.getItem('hydra_is_admin') === 'true' && !window.isAdmin) { window.isAdmin = true; // Synchronize global flag } if (!window.isAdmin) { alert('You must be an admin to edit'); window._editModeToggleInProgress = false; return; } const currentEditState = window.isInEditMode ? window.isInEditMode() : (typeof window.isEditing !== 'undefined' ? window.isEditing : false); const newEditState = !currentEditState; console.log(`Changing edit mode from ${currentEditState} to: ${newEditState}`); // Apply body class for edit mode styling if (newEditState) { console.log('Entering edit mode - setting body class and theme'); document.body.setAttribute('data-theme', 'pico'); // Optional: if Pico theme is used for edit mode document.body.classList.add('edit-mode-active'); } else { console.log('Exiting edit mode - removing body class and theme'); document.body.removeAttribute('data-theme'); document.body.classList.remove('edit-mode-active'); } // Update the global state and dispatch event if (typeof window.updateGlobalEditState === 'function') { window.updateGlobalEditState(newEditState); } else { window.isEditing = newEditState; // Fallback document.dispatchEvent(new CustomEvent('edit-mode-changed', { detail: { editing: newEditState } })); } const sidebarEditControls = document.querySelector('.edit-controls'); if (newEditState) { // Entering edit mode if (sidebarEditControls) { sidebarEditControls.style.display = 'flex'; sidebarEditControls.classList.add('visible'); } // Show logout button in edit controls when entering edit mode const logoutButton = document.getElementById('logoutButton'); if (logoutButton) { logoutButton.style.display = 'flex'; logoutButton.classList.add('visible'); } // Always render the tree-like menu in the sidebar for editing if (typeof window.renderGalleries === 'function') { console.log('toggleEditMode (Edit): Rendering sidebar menu (gallery tree) for editing.'); window.renderGalleries(); // This will show all items, including those with visible:false } setTimeout(() => { if (typeof window.initializeNestedSortables === 'function') { console.log('Initializing nested sortables for edit mode'); window.initializeNestedSortables(); } if (window.SidebarManager && typeof window.SidebarManager._reinitializeNestedSortables === 'function') { window.SidebarManager._reinitializeNestedSortables(); } }, 150); // If the currently active item was invisible, reload it now that we are in edit mode. if(window.galleries && window.activeGalleryId){ const currentGalleryItem = window.galleries.find(g => g.id === window.activeGalleryId); if (currentGalleryItem && currentGalleryItem.visible === false) { console.log('toggleEditMode (Edit): Active item was invisible, reloading it.'); if (currentGalleryItem.isPage && window.loadPage) { window.loadPage(window.activeGalleryId); } else if(window.loadGallery) { window.loadGallery(window.activeGalleryId); } } } if(typeof window.showStyleEditor === 'function') window.showStyleEditor(false); // Ensure style editor is hidden initially } else { // Exiting edit mode (going to Live view) if (sidebarEditControls) { sidebarEditControls.style.display = 'none'; sidebarEditControls.classList.remove('visible'); } // Hide logout button when exiting edit mode const logoutButton = document.getElementById('logoutButton'); if (logoutButton) { logoutButton.style.display = 'none'; logoutButton.classList.remove('visible'); } if(typeof window.closeAllForms === 'function') window.closeAllForms(); if (typeof window.destroyNestedSortables === 'function') { window.destroyNestedSortables(); } else if (window.sortableInstances && Array.isArray(window.sortableInstances)) { window.sortableInstances.forEach(instance => instance.destroy()); window.sortableInstances = []; } // Re-render the menu based on the actual view mode layout. // These functions should internally handle not showing items with visible:false. let currentLayout = 'sidebar'; if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) { currentLayout = window.MenuStyleCustomizer.settings.menuLayout || 'sidebar'; } if (currentLayout === 'horizontal' && typeof window.renderHorizontalMenu === 'function') { window.renderHorizontalMenu(); } else if (typeof window.renderGalleries === 'function') { window.renderGalleries(); } // Handle the currently active gallery/page. if (window.galleries && window.activeGalleryId) { const currentGalleryItem = window.galleries.find(g => g.id === window.activeGalleryId); if (currentGalleryItem) { // The content for currentGalleryItem should remain loaded, // even if currentGalleryItem.visible is false. // The menu rendering above will hide it from the list if it's invisible. // Apply page-specific menu hiding rules. const bodyEl = document.body; // When exiting edit mode, isInEditMode() will effectively be false for this check. if (currentGalleryItem.hideMenuOnPage) { bodyEl.classList.add('menu-hidden-on-page'); } else { bodyEl.classList.remove('menu-hidden-on-page'); } // Ensure active states in the (potentially now filtered) menu are updated. // If the active item is invisible, it won't be marked active in the menu, which is fine. if(typeof window.updateActiveStates === 'function') window.updateActiveStates(); if(typeof window.updateActiveStatesHorizontal === 'function') window.updateActiveStatesHorizontal(); } else { // activeGalleryId points to a non-existent item, so clear it and the content. console.log('toggleEditMode (Live): activeGalleryId points to a non-existent item. Clearing content.'); window.activeGalleryId = null; const galleryContainer = document.querySelector('.gallery-container'); if (galleryContainer) galleryContainer.innerHTML = ''; if(typeof window.updateActiveStates === 'function') window.updateActiveStates(); if(typeof window.updateActiveStatesHorizontal === 'function') window.updateActiveStatesHorizontal(); } } // If, after all that, no content is effectively active (e.g., initial load to root, // or activeGalleryId became null because the item was deleted) // then try to load a default page (home page or first visible gallery). const galleryContainer = document.querySelector('.gallery-container'); // Check if activeGalleryId is null OR if it's set but the container is empty (e.g. item was deleted) const noActiveContent = window.activeGalleryId === null || (galleryContainer && galleryContainer.innerHTML.trim() === ''); if (noActiveContent && window.galleries) { console.log("toggleEditMode (Live): No active content, attempting to load default page."); const homePage = window.galleries.find(g => g.isHomePage === true); let loadedDefault = false; if (homePage) { // Load home page regardless of its visibility status for this default loading logic console.log(`toggleEditMode (Live): Loading home page: ${homePage.title}`); if (homePage.isPage && window.loadPage) { window.loadPage(homePage.id); loadedDefault = true; } else if (window.loadGallery) { window.loadGallery(homePage.id); loadedDefault = true; } } if (!loadedDefault) { // If no home page, load the first *visible* gallery/page in live view. const firstVisibleGallery = window.galleries.find(g => g.visible !== false && !g.isSpacer && !g.isFolder && !g.isSubmenu); if (firstVisibleGallery) { console.log(`toggleEditMode (Live): No home page, loading first visible item: ${firstVisibleGallery.title}`); if (firstVisibleGallery.isPage && window.loadPage) { window.loadPage(firstVisibleGallery.id); } else if(window.loadGallery) { window.loadGallery(firstVisibleGallery.id); } } else { console.log('toggleEditMode (Live): No home page and no visible items to load.'); } } } } window._editModeToggleInProgress = false; } /** * Improved toggleSidebarElementForm - Closes other forms first */ function toggleSidebarElementForm() { const formId = 'sidebarElementForm'; const form = document.getElementById(formId); if (!form) return; // If this form is already open, just close everything if (window.currentOpenForm === formId) { closeAllForms(); return; } // Otherwise close all forms and open this one closeAllForms(); // Show the form form.style.display = 'block'; form.classList.add('visible'); // Set this as the current open form window.currentOpenForm = formId; } // New function to add a sidebar element function addSidebarElement() { const typeInput = document.querySelector('input[name="sidebarElementType"]:checked'); if (!typeInput) { alert('Please select an element type'); return; } const type = typeInput.value; // Use the SidebarManager to add the element if (window.SidebarManager) { const newElementId = window.SidebarManager.addElement(type); toggleSidebarElementForm(); if (newElementId) { // Save the changes to KV store saveGalleries().then(() => { console.log(`New ${type} element added successfully with ID: ${newElementId}`); }).catch(error => { console.error('Error saving new element:', error); }); } } else { alert('SidebarManager not available'); } } // Global variable to store metadata window.siteMetadata = { title: '', description: '', googleAnalytics: '', noIndex: false, favicon: '', contentLanguage: 'en', // Default to English copyrightEnabled: false, copyrightText: '' }; // Global variable to store uploaded favicon URL temporarily window.uploadedFaviconUrl = ''; function updateMetadataCopyrightFieldState() { const toggle = document.getElementById('metadataCopyrightEnabled'); const textInput = document.getElementById('metadataCopyrightText'); const isEnabled = !!(toggle && toggle.checked); if (textInput) { textInput.disabled = !isEnabled; } // Intentionally leave the surrounding form group in place; the disabled attribute on the input // provides the visual cue in PicoCSS. } /** * Improved toggleMetadataEditor - Closes other forms first */ function toggleMetadataEditor() { const formId = 'metadataEditor'; const editor = document.getElementById(formId); if (!editor) return; // If this form is already open, just close everything if (window.currentOpenForm === formId) { closeAllForms(); return; } // Otherwise close all forms and open this one closeAllForms(); // Load current values before showing loadMetadataValues(); editor.style.display = 'block'; // Set this as the current open form window.currentOpenForm = formId; } // Load metadata values into the form function loadMetadataValues() { // Get the current values from the global object const metadata = window.siteMetadata || {}; console.log('loadMetadataValues: Loading metadata:', JSON.stringify(metadata)); console.log('loadMetadataValues: Google Analytics from metadata:', metadata.googleAnalytics); console.warn('=== FAVICON LOAD DEBUG ==='); console.warn('window.siteMetadata:', JSON.stringify(window.siteMetadata)); console.warn('metadata object:', JSON.stringify(metadata)); console.warn('metadata.favicon value:', metadata.favicon); console.warn('metadata.favicon type:', typeof metadata.favicon); // Populate form fields document.getElementById('metaTitle').value = metadata.title || ''; document.getElementById('metaDescription').value = metadata.description || ''; document.getElementById('googleAnalytics').value = metadata.googleAnalytics || ''; document.getElementById('noIndexToggle').checked = metadata.noIndex || false; const copyrightToggle = document.getElementById('metadataCopyrightEnabled'); const copyrightInput = document.getElementById('metadataCopyrightText'); if (copyrightToggle) { const enabled = !!metadata.copyrightEnabled; copyrightToggle.checked = enabled; if (copyrightInput) { copyrightInput.value = metadata.copyrightText || ''; } } updateMetadataCopyrightFieldState(); // Set content language dropdown (defaults to 'en') const contentLanguageSelect = document.getElementById('contentLanguage'); if (contentLanguageSelect) { contentLanguageSelect.value = metadata.contentLanguage || 'en'; console.log('Loaded content language:', contentLanguageSelect.value); } const faviconInput = document.getElementById('faviconUrl'); if (faviconInput) { // Check if we have a recently uploaded favicon URL const faviconValue = metadata.favicon || window.uploadedFaviconUrl || ''; faviconInput.value = faviconValue; console.warn('Set favicon input value to:', faviconInput.value); console.warn('Used global uploaded favicon URL:', window.uploadedFaviconUrl); } else { console.error('Favicon input element not found!'); } // Show favicon preview if there's an existing favicon if (metadata.favicon) { console.warn('Showing favicon preview for:', metadata.favicon); showFaviconPreview(metadata.favicon); } else { console.warn('No favicon to preview'); } console.log('loadMetadataValues: Google Analytics field value after setting:', document.getElementById('googleAnalytics').value); } // Save metadata values function saveMetadata() { // Get values from form const googleAnalyticsElement = document.getElementById('googleAnalytics'); console.log('Google Analytics element found:', !!googleAnalyticsElement); console.log('Google Analytics element value:', googleAnalyticsElement ? googleAnalyticsElement.value : 'ELEMENT NOT FOUND'); const faviconInput = document.getElementById('faviconUrl'); const faviconValue = faviconInput ? faviconInput.value : ''; const contentLanguageSelect = document.getElementById('contentLanguage'); const contentLanguageValue = contentLanguageSelect ? contentLanguageSelect.value : 'en'; const copyrightToggle = document.getElementById('metadataCopyrightEnabled'); const copyrightInput = document.getElementById('metadataCopyrightText'); const copyrightEnabled = copyrightToggle ? copyrightToggle.checked : false; const copyrightText = copyrightInput ? copyrightInput.value.trim() : ''; console.warn('=== FAVICON SAVE DEBUG ==='); console.warn('Favicon input element exists:', !!faviconInput); console.warn('Favicon input value:', faviconValue); console.warn('Favicon input value length:', faviconValue.length); console.warn('=== CONTENT LANGUAGE SAVE ==='); console.warn('Content language selected:', contentLanguageValue); const metadata = { title: document.getElementById('metaTitle').value, description: document.getElementById('metaDescription').value, googleAnalytics: googleAnalyticsElement ? googleAnalyticsElement.value : '', noIndex: document.getElementById('noIndexToggle').checked, favicon: faviconValue, contentLanguage: contentLanguageValue, // Get from dropdown copyrightEnabled, copyrightText }; console.warn('Metadata object being created:', JSON.stringify(metadata)); console.warn('Favicon in metadata object:', metadata.favicon); // Update global metadata object immediately window.siteMetadata = metadata; console.log("Updated window.siteMetadata:", JSON.stringify(window.siteMetadata)); if (window.SidebarManager && typeof window.SidebarManager.updateMetadataFooter === 'function') { window.SidebarManager.updateMetadataFooter(); } // Ensure we have the latest galleries, sidebar elements, and styles // before constructing the customData object. // Synchronize galleries first if the function exists if (typeof synchronizeGalleries === 'function') { synchronizeGalleries(); console.log('Galleries synchronized within saveMetadata'); } // Retrieve current galleries (use window.galleries as the source of truth after sync) const currentGalleries = window.galleries || galleries || []; console.log(`Current galleries count before constructing customData: ${currentGalleries.length}`); // Retrieve current sidebar elements const currentSidebarElements = window.SidebarManager ? window.SidebarManager.elements : []; // Retrieve current menu styles const currentMenuStyles = window.MenuStyleCustomizer ? window.MenuStyleCustomizer.settings : {}; // Now construct customData using the retrieved current state if (window.saveGalleries) { const customData = { galleries: currentGalleries, // Use the synchronized/retrieved galleries siteMetadata: metadata, // Use the metadata gathered from the form sidebarElements: currentSidebarElements, // Use the retrieved sidebar elements menuStyles: currentMenuStyles // Use the retrieved menu styles }; console.log(`Saving metadata with preserved galleries: ${customData.galleries.length}`); console.log("Metadata being saved:", JSON.stringify(metadata)); console.log("Google Analytics specifically:", metadata.googleAnalytics); console.log("Sidebar elements being saved:", customData.sidebarElements.length); console.log("Menu styles being saved:", JSON.stringify(customData.menuStyles)); // Save with our correctly constructed custom data object window.saveGalleries(customData) .then(() => { alert('Metadata saved successfully'); toggleMetadataEditor(); // Hide the editor }) .catch(error => { console.error('Error saving metadata:', error); alert('Error saving metadata. Please try again.'); }); } else { alert('Save function not available'); } } // Function to add new entries to the style editor function showStyleEditor(display = false) { // Check if style editor already exists let styleEditor = document.getElementById('menuStyleEditor'); if (!styleEditor) { // Get the edit controls element (to place the editor after it) const editControls = document.querySelector('.edit-controls'); if (editControls && window.MenuStyleCustomizer) { // Create a wrapper for the style editor styleEditor = document.createElement('div'); styleEditor.id = 'menuStyleEditor'; styleEditor.style.display = 'none'; // Insert the editor after the edit controls editControls.parentNode.insertBefore(styleEditor, editControls.nextSibling); // Create the style editor UI window.MenuStyleCustomizer.createStyleEditor(styleEditor); } } // Only show the editor if display parameter is true if (styleEditor && display) { styleEditor.style.display = 'block'; } } // New function to hide the style editor function hideStyleEditor() { const styleEditor = document.getElementById('menuStyleEditor'); if (styleEditor) { styleEditor.style.display = 'none'; } } /** * Improved toggleAddForm - Closes other forms first and ensures proper sizing */ function toggleAddForm() { const formId = 'addForm'; const form = document.getElementById(formId); if (!form) return; // If this form is already open, just close everything if (window.currentOpenForm === formId) { closeAllForms(); return; } // Otherwise close all forms and open this one closeAllForms(); // Determine the appropriate max-height based on content // First check if we're showing the page form by checking the radio button const isPageSelected = document.querySelector('input[name="galleryType"][value="page"]')?.checked; // Allow extra space for the page form const maxHeight = isPageSelected ? '600px' : '500px'; // Show the form with proper max height form.style.maxHeight = maxHeight; form.style.overflow = 'visible'; // Ensure all content is visible form.classList.add('visible'); // Also add inline styles to ensure visibility during animation form.style.padding = '15px'; // Force recalculation of max-height after UI is visible setTimeout(() => { // Get actual content height + padding const contentHeight = form.scrollHeight + 30; // Add padding // Set max-height based on content with minimum of 500px form.style.maxHeight = Math.max(contentHeight, 500) + 'px'; console.log(`Setting add form max height to ${form.style.maxHeight}`); }, 50); // Populate parent options const parentSelect = document.getElementById('galleryParent'); if (parentSelect) { parentSelect.innerHTML = ''; // Add all galleries that could be parents (including submenus) galleries.forEach(gallery => { parentSelect.innerHTML += ``; }); } // Set this as the current open form window.currentOpenForm = formId; } // Store the currently open form ID window.currentOpenForm = null; /** * Close all editor forms with improved handling for add form */ function closeAllForms() { // Add Form - Special handling to ensure proper animation const addForm = document.getElementById('addForm'); if (addForm) { addForm.classList.remove('visible'); // Also force style reset after animation completes setTimeout(() => { if (!addForm.classList.contains('visible')) { addForm.style.maxHeight = '0'; addForm.style.overflow = 'hidden'; addForm.style.padding = '0 15px'; } }, 350); // Set slightly longer than CSS transition } // Style Editor const styleEditor = document.getElementById('menuStyleEditor'); if (styleEditor) { styleEditor.style.display = 'none'; } // Metadata Editor const metadataEditor = document.getElementById('metadataEditor'); if (metadataEditor) { metadataEditor.style.display = 'none'; } // Sidebar Element Form const sidebarElementForm = document.getElementById('sidebarElementForm'); if (sidebarElementForm) { sidebarElementForm.style.display = 'none'; sidebarElementForm.classList.remove('visible'); } // Import Classic Form const importClassicForm = document.getElementById('importClassicForm'); if (importClassicForm) { importClassicForm.style.display = 'none'; importClassicForm.classList.remove('visible'); } // Clear the current open form window.currentOpenForm = null; } function downloadJson() { const data = JSON.stringify(galleries, null, 2); const blob = new Blob([data], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'galleries.json'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } let galleriesBackup = []; /** * Improved version of initializeNestedSortables with a better nesting approach */ function initializeNestedSortables() { console.log('Initializing nested sortables with improved nesting logic...'); // Create a backup of galleries try { galleriesBackup = JSON.parse(JSON.stringify(galleries)); } catch (error) { console.error('Error creating galleries backup:', error); galleriesBackup = []; } // Find all nested sortable lists const nestedSortables = document.querySelectorAll('.nested-sortable'); if (nestedSortables.length === 0) { console.warn('No nested sortable elements found! Check your HTML structure.'); return; } // Helper: compute depth based on ancestor nested lists function computeDepth(liEl) { let depth = 0; let node = liEl; while (node) { const parentOl = node.closest('ol.nested-sortable'); if (!parentOl) break; const parentLi = parentOl.closest('li'); if (parentLi) { depth += 1; node = parentLi; } else { break; } } // Top-level depth should be 0 return Math.max(depth - 1, 0); } // Helper: apply indentation styling to an li and its subtree function applyIndentationFromDepth(rootLi) { if (!rootLi) return; const applyToItem = (li) => { const depth = computeDepth(li); // Update data-nesting-level attribute li.setAttribute('data-nesting-level', depth); // Apply padding-left style const content = li.querySelector('.gallery-item-content'); if (content) { content.style.paddingLeft = (depth * 16) + 'px'; } }; applyToItem(rootLi); rootLi.querySelectorAll('li').forEach(applyToItem); } // Helper: ensure parent has proper state and nested list function ensureParentChildState(parentLi) { if (!parentLi) return null; parentLi.classList.add('has-children'); let nestedList = parentLi.querySelector('ol.nested-sortable'); if (!nestedList) { nestedList = document.createElement('ol'); nestedList.className = 'nested-sortable'; parentLi.appendChild(nestedList); } return nestedList; } // Helper: clean parent if it has no children function cleanupParentIfEmpty(parentLi) { if (!parentLi) return; const nestedList = parentLi.querySelector('ol.nested-sortable'); if (nestedList && nestedList.children.length === 0) { nestedList.remove(); parentLi.classList.remove('has-children'); } } // Initialize each sortable with improved nesting settings let sortableInstances = []; for (let i = 0; i < nestedSortables.length; i++) { try { const container = nestedSortables[i]; // Track where the placeholder indicates the item should be placed // This helps us detect when SortableJS incorrectly auto-nests based on mouse position let intendedPlacementContainer = null; let intendedRelatedElement = null; // Track last move to prevent oscillation let lastMoveTime = 0; let lastMoveRelatedId = null; const MOVE_DEBOUNCE_MS = 50; // Minimum time between moves (in milliseconds) // Track if we're dragging the last item in a folder (to prevent boundary oscillation) let isLastItemInFolder = false; let draggedItemOriginalContainer = null; // Track container changes to prevent rapid switching (hysteresis) let firstPositionFixLoopActive = false; // Prevent multiple fix loops let lastContainerDecision = null; // 'inside' or 'outside' let lastContainerDecisionTime = 0; const CONTAINER_SWITCH_DEBOUNCE_MS = 150; // Longer debounce for container switches const sortable = new Sortable(container, { group: 'nested', animation: 150, // Reduced animation during drag to reduce lag easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', // Smooth easing function fallbackOnBody: false, // Disable forced fallback on desktop for better performance forceFallback: false, // Use native drag on desktop // CRITICAL: These settings make nesting much easier // Higher swap threshold makes it easier to drag over items swapThreshold: 0.65, // Increased to reduce oscillation within same folder // IMPROVED NESTING SETTINGS // Used when determining if an item should be nested invertSwap: false, // Disable to reduce oscillation direction: 'vertical', // Lock direction to vertical to reduce oscillation // Use handle for dragging handle: '.nested-sortable-handle', // Distinguish between click and drag delay: 120, delayOnTouchOnly: true, // Empty container settings emptyInsertThreshold: 10, // Add custom ghost class ghostClass: 'sortable-ghost', chosenClass: 'sortable-chosen', dragClass: 'sortable-drag', // Make sure we listen to external move events dragoverBubble: true, // Store original dimensions to prevent layout shifts preventOnFilter: false, // Better drag preview stability scroll: true, scrollSensitivity: 100, scrollSpeed: 20, // Reduce oscillation by making placeholder movement less sensitive forceFallback: false, // Use native drag for better performance fallbackTolerance: 0, // No tolerance - use exact position /** * NESTING LOGIC: Prevent auto-nesting based on hover * Instead, rely on SortableJS's placeholder position which is accurate * We'll verify and correct placement in onEnd based on actual DOM position */ onMove: function(evt, originalEvent) { const dragged = evt.dragged; const related = evt.related; if (!dragged || !related) return true; // Safety check: Ensure items have data-id attributes // This can be missing if item was just added and DOM not fully updated if (!dragged.dataset.id || !related.dataset.id) { console.warn('⚠️ [Drag] Item missing data-id attribute, aborting drag', { draggedHasId: !!dragged.dataset.id, relatedHasId: !!related.dataset.id }); return false; } // Get the IDs to check for circular references const draggedId = parseInt(dragged.dataset.id); const relatedId = parseInt(related.dataset.id); // Don't allow dropping inside itself or its descendants if (draggedId === relatedId || hasAncestor(relatedId, draggedId)) { return false; } const currentTime = Date.now(); const rootContainer = document.querySelector('#galleryTree > ol.nested-sortable'); const draggedParent = draggedItemOriginalContainer || dragged.parentElement; // Determine if we're moving inside or outside a folder // IMPORTANT: Check if related element IS the folder item itself (parent li) // When targeting first position, SortableJS might use the folder item as related const isRelatedFolderItem = related.classList && related.classList.contains('has-children'); let actualRelatedParent = related.parentElement; // If related IS the folder item, we need to get its nested sortable container if (isRelatedFolderItem) { const nestedSortable = related.querySelector('ol.nested-sortable'); if (nestedSortable) { actualRelatedParent = nestedSortable; } } const relatedParent = actualRelatedParent; const isMovingInsideFolder = relatedParent && relatedParent.classList.contains('nested-sortable') && relatedParent !== rootContainer; const isMovingOutsideFolder = !isRelatedFolderItem && relatedParent === rootContainer; const currentDecision = isMovingInsideFolder ? 'inside' : (isMovingOutsideFolder ? 'outside' : null); // Determine if we're moving within the same folder - this is the key check // If related IS the folder item, we're still moving within the same folder let isMovingWithinSameFolder = (isMovingInsideFolder && draggedParent && draggedParent.classList.contains('nested-sortable') && draggedParent !== rootContainer && relatedParent === draggedParent) || // OR: if related is the folder item itself and we're dragging from that folder (isRelatedFolderItem && draggedParent && draggedParent.classList.contains('nested-sortable') && draggedParent !== rootContainer && related.querySelector('ol.nested-sortable') === draggedParent); // Special case: If related is the folder item, we're definitely trying to move within that folder // This happens when targeting first position - SortableJS uses the folder item as related if (isRelatedFolderItem && draggedParent && draggedParent.classList.contains('nested-sortable') && draggedParent !== rootContainer) { const folderNestedSortable = related.querySelector('ol.nested-sortable'); if (folderNestedSortable === draggedParent) { // We're dragging within the folder, and related is the folder item // This means we're targeting first position - FORCE it to be treated as same-folder move // FORCE same-folder detection - override any previous calculation isMovingWithinSameFolder = true; } } // DEBUG: Log placeholder position changes ONLY when dragging from within a folder AND something interesting happens // (Don't log every single move - too noisy) if (draggedParent && draggedParent.classList.contains('nested-sortable') && draggedParent !== rootContainer) { const folderItems = Array.from(draggedParent.children); const draggedIndex = folderItems.indexOf(dragged); const relatedIndex = folderItems.indexOf(related); // Get placeholder element to see where it actually is const placeholder = document.querySelector('.sortable-ghost'); const placeholderParent = placeholder ? placeholder.parentElement : null; const placeholderIndex = placeholderParent && placeholderParent.classList.contains('nested-sortable') ? Array.from(placeholderParent.children).indexOf(placeholder) : -1; const folderItem = draggedParent.closest('li.has-children'); const folderItemId = folderItem ? folderItem.dataset.id : null; const firstItemInFolder = folderItems.length > 0 ? folderItems[0] : null; const firstItemInFolderId = firstItemInFolder ? firstItemInFolder.dataset.id : null; // Check if we're trying to target first position // related could be the first item OR the folder item itself (when SortableJS uses folder as related) const isFirstPosition = folderItems.length > 0 && related === firstItemInFolder; const isRelatedFolderItemCheck = related === folderItem; const isTargetingFirstViaFolderItem = isRelatedFolderItemCheck && !!folderItem; // FIXED: was assigning folderItem instead of boolean // When targeting first via folder item, placeholder should be at index 0 // But we're seeing it at index 1 - this is the issue! const shouldBeAtFirstPosition = isTargetingFirstViaFolderItem && placeholderIndex === 1 && draggedIndex > 0; // Check if placeholder is outside the folder when it should be inside const placeholderIsOutside = placeholderParent === rootContainer || (placeholderParent && !placeholderParent.classList.contains('nested-sortable')); const shouldBeInside = draggedIndex >= 0; // We're dragging from inside // Only log when there's a potential issue or interesting state change const placeholderMismatch = placeholderIndex !== relatedIndex && placeholderIndex >= 0 && relatedIndex >= 0; const jumpingOut = !isMovingWithinSameFolder && draggedIndex >= 0 && relatedIndex < 0; const jumpingOutToRoot = placeholderIsOutside && shouldBeInside; // First position issue: trying to target first but: // - placeholder is outside OR // - placeholder is at index 1 instead of 0 (when targeting first via folder item) const firstPositionIssue = (isFirstPosition || isTargetingFirstViaFolderItem) && (jumpingOut || jumpingOutToRoot || placeholderIsOutside || shouldBeAtFirstPosition); // ALWAYS log when targeting first position via folder item (this is the key case we're debugging) // OR when placeholder jumps out when it should stay in // OR when placeholder should be at first position but isn't const shouldLog = placeholderMismatch || jumpingOut || jumpingOutToRoot || firstPositionIssue || isTargetingFirstViaFolderItem || shouldBeAtFirstPosition; if (shouldLog) { console.error('🔍 [Placeholder Position]', { draggedId, draggedIndex, relatedId, relatedIndex, placeholderIndex, relatedElementTag: related.tagName, isRelatedFolderItem: isRelatedFolderItemCheck, isTargetingFirstViaFolderItem, isMovingWithinSameFolder, placeholderMismatch, jumpingOut, jumpingOutToRoot, isFirstPosition, placeholderIsOutside, placeholderParentType: placeholderParent === rootContainer ? 'ROOT' : (placeholderParent && placeholderParent.classList.contains('nested-sortable') ? 'FOLDER' : 'OTHER'), folderItemId, firstItemInFolderId, firstPositionIssue, shouldBeAtFirstPosition, expectedPlaceholderIndex: isTargetingFirstViaFolderItem ? 0 : placeholderIndex }); } } // CRITICAL: For moves within the same folder, skip ALL the complex boundary logic // This allows smooth reordering just like items not in folders if (isMovingWithinSameFolder) { // Simple debounce only - same as root level items const timeSinceLastMove = currentTime - lastMoveTime; // Only prevent very rapid oscillation to same element if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.5 && relatedId === lastMoveRelatedId) { return false; } // Allow all moves within same folder - update tracking and continue // SPECIAL FIX: When targeting first position via folder item, fix placeholder position // SortableJS places it at index 1 when related is the folder item, but we want it at index 0 // BUT: Only apply this fix when actually near/at first position to avoid being too aggressive if (isRelatedFolderItem && draggedParent && draggedParent.classList.contains('nested-sortable') && draggedParent !== rootContainer) { const folderItems = Array.from(draggedParent.children); const firstItem = folderItems.length > 0 ? folderItems[0] : null; // Only apply fix if we're actually near first position (draggedIndex is small, indicating we're moving up) // This prevents the fix from being too aggressive when moving to other positions const isNearFirstPosition = draggedIndex <= 2; // Allow fix when within first 3 positions if (firstItem && firstItem !== dragged && isNearFirstPosition) { // Fix immediately (synchronously) first - don't wait for next frame const placeholder = document.querySelector('.sortable-ghost'); if (placeholder && placeholder.parentElement === draggedParent) { const currentIndex = Array.from(draggedParent.children).indexOf(placeholder); // Only fix if placeholder is at index 1 when we're targeting first position // Don't force it if it's at index 0 or beyond index 2 (user might be moving to position 2 or 3) if (currentIndex === 1 && draggedIndex <= 1) { draggedParent.insertBefore(placeholder, firstItem); } } } else { // Not near first position - stop any active fix loop firstPositionFixLoopActive = false; } } else { // Not targeting first position anymore - reset flag firstPositionFixLoopActive = false; } lastMoveTime = currentTime; lastMoveRelatedId = relatedId; // Skip all the boundary case logic below - just continue to allow the move // Fall through to update intendedPlacementContainer and return true } else { // Only apply complex boundary logic for moves BETWEEN folders or to/from root // Hysteresis: Prevent rapid switching between inside/outside folder const targetFolderItems = isMovingInsideFolder ? Array.from(relatedParent.children) : []; const isFirstPositionInFolder = isMovingInsideFolder && targetFolderItems.length > 0 && related === targetFolderItems[0]; if (currentDecision && lastContainerDecision && currentDecision !== lastContainerDecision) { const timeSinceContainerSwitch = currentTime - lastContainerDecisionTime; // Make first position easier to target from outside if (isFirstPositionInFolder) { // Moving to first position from outside - use shorter debounce const effectiveDebounce = CONTAINER_SWITCH_DEBOUNCE_MS * 0.3; if (timeSinceContainerSwitch < effectiveDebounce) { // Only block if it's very rapid (oscillation) if (timeSinceContainerSwitch < MOVE_DEBOUNCE_MS) { return false; } } } else { // Normal container switch - apply full hysteresis if (timeSinceContainerSwitch < CONTAINER_SWITCH_DEBOUNCE_MS) { return false; } } } } // Special handling for boundary cases (last item in folder, or dragging into folder) // BUT: Skip all this logic if we're moving within the same folder (already handled above) if (!isMovingWithinSameFolder && draggedParent && draggedParent.classList.contains('nested-sortable') && draggedParent !== rootContainer) { const folderItem = draggedParent.closest('li.has-children'); const currentFolderItems = Array.from(draggedParent.children); const isFirstPositionInCurrentFolder = currentFolderItems.length > 0 && currentFolderItems[0] !== dragged && (related === currentFolderItems[0] || (isMovingInsideFolder && relatedParent === draggedParent && related === currentFolderItems[0])); // Case 0: This case is now handled earlier for same-folder moves // Only handle cross-folder moves here // Case 1: Dragging last item out of folder if (isLastItemInFolder && isMovingOutsideFolder) { const timeSinceLastMove = currentTime - lastMoveTime; // BUT: If we're trying to move to first position, don't block it // Check if the related element is the folder itself or right after it if (folderItem) { const folderNextSibling = folderItem.nextElementSibling; // If we're moving to position right after folder, it might be trying to go to first position // Allow it if it's been a reasonable time if (folderNextSibling && related === folderNextSibling) { // This is ambiguous - could be first position or outside // Require more time but not as much as pure outside move if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 2) { if (relatedId === lastMoveRelatedId) { return false; } } } else { // Definitely moving outside - require longer debounce if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 2.5) { if (relatedId === lastMoveRelatedId) { return false; // Reject if oscillating } } } } else { // No folder item found - apply standard debounce if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 2.5) { if (relatedId === lastMoveRelatedId) { return false; } } } } // Case 2: Dragging item into folder from outside if (isMovingInsideFolder && relatedParent !== draggedParent) { const targetFolderItems = Array.from(relatedParent.children); const isNearBottom = targetFolderItems.length > 0 && (related === targetFolderItems[targetFolderItems.length - 1] || targetFolderItems.indexOf(related) >= targetFolderItems.length - 1); const isFirstPos = targetFolderItems.length > 0 && related === targetFolderItems[0]; // Moving to first position - make it easy if (isFirstPos) { const timeSinceLastMove = currentTime - lastMoveTime; // Very short debounce for first position if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.3 && relatedId === lastMoveRelatedId) { return false; } // Allow move to first position } else if (isNearBottom) { // For bottom position, still use some debounce to prevent oscillation const timeSinceLastMove = currentTime - lastMoveTime; if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 1.5) { if (relatedId === lastMoveRelatedId) { return false; } } } } // Case 2.5: Prevent jumping OUT of folder when trying to move to first/last position within // This is tricky - SortableJS might detect it as outside when we're actually targeting first/last if (isMovingOutsideFolder && folderItem && draggedParent === draggedItemOriginalContainer) { // We're dragging from within a folder but detected as moving outside // Check if the related element is adjacent to the folder (which might indicate targeting first/last) const folderIndex = Array.from(rootContainer.children).indexOf(folderItem); const relatedIndex = Array.from(rootContainer.children).indexOf(related); // If related is immediately before or after folder, might be targeting first/last if (folderIndex >= 0 && relatedIndex >= 0) { const isAdjacentToFolder = (relatedIndex === folderIndex - 1) || (relatedIndex === folderIndex + 1); if (isAdjacentToFolder) { // Might be targeting first/last position - be lenient const timeSinceLastMove = currentTime - lastMoveTime; // Only block if it's very rapid oscillation if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.4 && relatedId === lastMoveRelatedId) { return false; } // Otherwise allow - user might be trying to target first/last position // Don't block based on this check } } } } // Case 3: Dragging from root into folder if (draggedParent === rootContainer && isMovingInsideFolder) { const targetFolderItems = Array.from(relatedParent.children); const isNearBottom = targetFolderItems.length > 0 && (related === targetFolderItems[targetFolderItems.length - 1] || targetFolderItems.indexOf(related) >= targetFolderItems.length - 1); const isFirstPosition = targetFolderItems.length > 0 && related === targetFolderItems[0]; // Special handling for first position - make it easier to target if (isFirstPosition) { const timeSinceLastMove = currentTime - lastMoveTime; // Make first position easier - only prevent very rapid oscillation if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.5) { if (relatedId === lastMoveRelatedId) { return false; } } // Allow move to first position - don't block it // Continue to track but don't apply strict debouncing } else if (isNearBottom) { // Keep strict debouncing for bottom position const timeSinceLastMove = currentTime - lastMoveTime; if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 2) { if (relatedId === lastMoveRelatedId) { return false; } } } } // Case 4: Prevent jumping FROM first position to outside folder (but allow easy access TO first position) // If we're at first position and trying to move outside, require deliberate movement if (isMovingInsideFolder) { const currentFolderItems = Array.from(relatedParent.children); const isFirstPos = currentFolderItems.length > 0 && related === currentFolderItems[0]; // If we're moving FROM first position to outside (reverse hysteresis) if (isFirstPos && lastContainerDecision === 'outside') { const timeSinceLastMove = currentTime - lastMoveTime; // If we just moved outside, require more time before allowing back to first position // BUT: if it's been a reasonable time, allow it (don't make it too hard) if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.8 && relatedId === lastMoveRelatedId) { // Very rapid - might be oscillation return false; } } } // Case 5: Prevent oscillation between first and second position within folder // BUT: Only apply if NOT moving within same folder (to allow smooth reordering) if (isMovingInsideFolder && relatedParent && !isMovingWithinSameFolder) { const folderItems = Array.from(relatedParent.children); const isFirstPos = folderItems.length > 0 && related === folderItems[0]; const isSecondPos = folderItems.length > 1 && related === folderItems[1]; // If moving from first to second (or vice versa) very rapidly, prevent oscillation if ((isFirstPos || isSecondPos) && lastMoveRelatedId) { const lastRelated = document.querySelector('li[data-id="' + lastMoveRelatedId + '"]'); if (lastRelated && lastRelated.parentElement === relatedParent) { const lastRelatedIndex = folderItems.indexOf(lastRelated); const isOscillating = (isFirstPos && lastRelatedIndex === 1) || (isSecondPos && lastRelatedIndex === 0); if (isOscillating) { const timeSinceLastMove = currentTime - lastMoveTime; // Allow moving to first position easily, but prevent rapid oscillation between first/second if (isSecondPos && timeSinceLastMove < MOVE_DEBOUNCE_MS * 1.2) { return false; // Prevent jumping from first to second too easily } // But allow first position more easily if (isFirstPos && timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.6 && relatedId === lastMoveRelatedId) { return false; // Only prevent very rapid oscillation } } } } } // Prevent rapid oscillation when dragging within same container // NOTE: For same-folder moves, we already updated tracking above, so skip this section if (!isMovingWithinSameFolder) { const timeSinceLastMove = currentTime - lastMoveTime; // If moving to the same related element very quickly, reject to prevent oscillation if (timeSinceLastMove < MOVE_DEBOUNCE_MS && relatedId === lastMoveRelatedId) { return false; } // Update tracking variables (only for non-same-folder moves) lastMoveTime = currentTime; lastMoveRelatedId = relatedId; } // For same-folder moves, tracking was already updated in the earlier block // Update container decision tracking if (currentDecision) { if (currentDecision !== lastContainerDecision) { lastContainerDecisionTime = currentTime; } lastContainerDecision = currentDecision; } // Track where the placeholder indicates the item should be placed // The related element is next to where the placeholder will be positioned // Check if it's at root level or nested level if (related) { intendedRelatedElement = related; const relatedParent = related.parentElement; const rootContainer = document.querySelector('#galleryTree > ol.nested-sortable'); // Determine if the placeholder is at root level or nested level if (relatedParent === rootContainer) { // Placeholder is at root level (not nested) intendedPlacementContainer = rootContainer; } else if (relatedParent && relatedParent.classList.contains('nested-sortable')) { // Placeholder is in a nested list (inside a folder) intendedPlacementContainer = relatedParent; } // Update ghost element indentation to match the target position requestAnimationFrame(() => { const ghost = document.querySelector('.sortable-ghost'); if (ghost && intendedPlacementContainer) { const ghostContent = ghost.querySelector('.gallery-item-content'); if (ghostContent) { // Calculate the depth where the ghost will be placed // Use the related element to determine the depth (if it exists and is in the target container) let targetDepth = 0; if (intendedPlacementContainer === rootContainer) { targetDepth = 0; // Root level } else if (related && related.parentElement === intendedPlacementContainer) { // Use the related element's depth to determine target depth targetDepth = computeDepth(related); } else { // Fallback: find first child in the container or calculate from parent folder const firstChild = intendedPlacementContainer.querySelector('li'); if (firstChild) { targetDepth = computeDepth(firstChild); } else { const parentFolder = intendedPlacementContainer.closest('li.has-children'); if (parentFolder) { targetDepth = computeDepth(parentFolder) + 1; } } } ghostContent.style.paddingLeft = (targetDepth * 16) + 'px'; } } }); } // Let SortableJS handle the placement based on placeholder position // We'll verify and correct if needed in onEnd return true; // Allow the move }, // Capture indentation before drag starts (fires when item is chosen) onChoose: function(evt) { if (!evt.item) return; const draggedItem = evt.item; const content = draggedItem.querySelector('.gallery-item-content'); // Store the original padding-left for the ghost if (content) { const computedStyle = window.getComputedStyle(content); const originalPaddingLeft = computedStyle.paddingLeft; draggedItem.dataset.originalPaddingLeft = originalPaddingLeft; } }, // Visual feedback on drag start onStart: function(evt) { if (!evt.item) return; // Don't set dimensions on the dragged item - let it maintain natural size // Setting width/height can cause layout shifts and menu expansion const draggedItem = evt.item; // Store original container and check if it's the last item in folder draggedItemOriginalContainer = draggedItem.parentElement; const rootContainer = document.querySelector('#galleryTree > ol.nested-sortable'); if (draggedItemOriginalContainer && draggedItemOriginalContainer.classList.contains('nested-sortable') && draggedItemOriginalContainer !== rootContainer) { const siblings = Array.from(draggedItemOriginalContainer.children); isLastItemInFolder = siblings.length > 0 && siblings[siblings.length - 1] === draggedItem; } else { isLastItemInFolder = false; } // Apply the stored padding-left to the ghost element // The ghost is created by SortableJS at this point requestAnimationFrame(() => { const ghost = document.querySelector('.sortable-ghost'); if (ghost) { const ghostContent = ghost.querySelector('.gallery-item-content'); const originalPaddingLeft = draggedItem.dataset.originalPaddingLeft; if (ghostContent && originalPaddingLeft) { ghostContent.style.paddingLeft = originalPaddingLeft; } } }); // Reset oscillation tracking variables lastMoveTime = 0; lastMoveRelatedId = null; lastContainerDecision = null; lastContainerDecisionTime = 0; // Create a backup try { window._dragStartElementIds = []; document.querySelectorAll('.nested-sortable li').forEach(li => { if (li.dataset.id) { window._dragStartElementIds.push(li.dataset.id); } }); galleriesBackup = JSON.parse(JSON.stringify(galleries)); } catch (error) { console.error('Error creating backup:', error); } // Add indicator classes document.body.classList.add('menu-item-dragging'); // Add hover indicator class to submenu items document.querySelectorAll('.has-children').forEach(item => { item.classList.add('potential-parent'); }); // Prevent layout recalculation during drag document.body.style.setProperty('--is-dragging', '1'); }, // Cleanup on end onEnd: function(evt) { // Verify and correct placement based on placeholder's intended position // SortableJS may auto-nest incorrectly based on mouse position, even if placeholder shows root level const dragged = evt.item; const previousParentLi = evt.from ? evt.from.closest('li') : null; const rootContainer = document.querySelector('#galleryTree > ol.nested-sortable'); if (!dragged || !rootContainer) { intendedPlacementContainer = null; intendedRelatedElement = null; // Restore normal sizing dragged.style.width = ''; dragged.style.height = ''; dragged.style.minHeight = ''; document.body.style.removeProperty('--is-dragging'); return; } // Get the actual parent container where SortableJS placed the item const actualParentList = dragged.parentElement; // Restore normal sizing - use requestAnimationFrame to prevent layout thrashing requestAnimationFrame(() => { dragged.style.width = ''; dragged.style.height = ''; dragged.style.minHeight = ''; document.body.style.removeProperty('--is-dragging'); }); // Check if the intended placement (where placeholder showed) differs from actual placement // This happens when SortableJS auto-nests based on mouse position over a folder if (intendedPlacementContainer && actualParentList !== intendedPlacementContainer) { // The item ended up in a different container than the placeholder indicated // Move it to where the placeholder showed it should be (next to intendedRelatedElement) if (intendedPlacementContainer === rootContainer) { // Placeholder was at root level - move item to root, positioned next to related element if (intendedRelatedElement && intendedRelatedElement.parentElement === rootContainer) { // Insert before the related element (where placeholder was) rootContainer.insertBefore(dragged, intendedRelatedElement); console.log("Corrected placement: moved from folder to root level where placeholder indicated"); } else if (intendedRelatedElement) { // Related element exists but not at root - find its folder and insert before it at root const folderItem = intendedRelatedElement.closest('li.has-children'); if (folderItem && folderItem.parentElement === rootContainer) { rootContainer.insertBefore(dragged, folderItem); console.log("Corrected placement: moved to root level above folder where placeholder indicated"); } else { rootContainer.appendChild(dragged); } } else { rootContainer.appendChild(dragged); } // Update indentation since it's now at root applyIndentationFromDepth(dragged); } else { // Placeholder was in a nested list - move it there if (intendedRelatedElement && intendedRelatedElement.parentElement === intendedPlacementContainer) { // Insert before the related element in the nested list intendedPlacementContainer.insertBefore(dragged, intendedRelatedElement); } else { intendedPlacementContainer.appendChild(dragged); } console.log("Corrected placement: moved to nested list where placeholder indicated"); // Update indentation for nested item applyIndentationFromDepth(dragged); // Ensure parent folder has proper structure const folderItem = intendedPlacementContainer.closest('li.has-children'); if (folderItem) { const nestedList = folderItem.querySelector('ol.nested-sortable'); if (nestedList && intendedPlacementContainer !== nestedList) { nestedList.appendChild(dragged); } const parentDepth = computeDepth(folderItem); const nestedDepth = parentDepth + 1; if (nestedList) { nestedList.className = 'nested-sortable nested-level-' + nestedDepth; } folderItem.classList.add('has-children'); } } } else if (actualParentList && actualParentList.classList.contains('nested-sortable') && actualParentList !== rootContainer) { // Item is correctly in a nested list - ensure proper structure const folderItem = actualParentList.closest('li.has-children'); if (folderItem) { const nestedList = folderItem.querySelector('ol.nested-sortable'); if (nestedList && actualParentList !== nestedList) { nestedList.appendChild(dragged); } const parentDepth = computeDepth(folderItem); const nestedDepth = parentDepth + 1; if (nestedList) { nestedList.className = 'nested-sortable nested-level-' + nestedDepth; } folderItem.classList.add('has-children'); } applyIndentationFromDepth(dragged); } else { // Item is correctly at root level applyIndentationFromDepth(dragged); } // Reset tracking variables intendedPlacementContainer = null; intendedRelatedElement = null; isLastItemInFolder = false; draggedItemOriginalContainer = null; lastContainerDecision = null; lastContainerDecisionTime = 0; // Apply indentation updates for ALL affected items after drag // Use requestAnimationFrame to batch DOM updates and prevent visual jumps requestAnimationFrame(() => { if (dragged) { // Update the dragged item and all its descendants applyIndentationFromDepth(dragged); // Update all items in the new container (siblings of dragged item) const newContainer = dragged.parentElement; if (newContainer && newContainer.classList.contains('nested-sortable')) { const siblings = Array.from(newContainer.children); siblings.forEach(sibling => { if (sibling !== dragged && sibling.tagName === 'LI') { applyIndentationFromDepth(sibling); } }); } // Update all items in the old container (if it still exists and has items) if (evt.from && evt.from.classList.contains('nested-sortable')) { const oldSiblings = Array.from(evt.from.children); oldSiblings.forEach(sibling => { if (sibling.tagName === 'LI') { applyIndentationFromDepth(sibling); } }); } // Also update all items in the entire tree to ensure consistency // This handles edge cases where indentation might be off const allItems = document.querySelectorAll('#galleryTree .nested-sortable li'); allItems.forEach(li => { const depth = computeDepth(li); li.setAttribute('data-nesting-level', depth); const content = li.querySelector('.gallery-item-content'); if (content) { content.style.paddingLeft = (depth * 16) + 'px'; } }); } }); // Clean up previous parent if it lost its last child if (previousParentLi) { cleanupParentIfEmpty(previousParentLi); } // Remove all indicator classes document.body.classList.remove('menu-item-dragging'); document.querySelectorAll('.potential-parent, .will-accept-child, .will-be-nested').forEach(el => { el.classList.remove('potential-parent', 'will-accept-child', 'will-be-nested'); }); // Standard cleanup for ghost elements document.querySelectorAll('.sortable-ghost, .sortable-chosen, .sortable-drag').forEach(item => { item.classList.remove('sortable-ghost', 'sortable-chosen', 'sortable-drag'); }); // Check if drop was outside a valid container if (!evt.to || !evt.to.classList.contains('nested-sortable')) { console.warn('Item was dropped outside a valid container - reverting'); restoreFromBackup(); return; } // Verify no elements were lost try { // Check DOM elements const endElementIds = []; document.querySelectorAll('.nested-sortable li').forEach(li => { if (li.dataset.id) { endElementIds.push(li.dataset.id); } }); if (window._dragStartElementIds && window._dragStartElementIds.length > 0) { const startCount = window._dragStartElementIds.length; const endCount = endElementIds.length; if (endCount < startCount) { console.error(`DOM elements were lost during drag! Original: ${startCount}, New: ${endCount}`); restoreFromBackup(); return; } } // Update the data structure updateGalleryStructure(); // Verify data model const originalCount = countGalleries(galleriesBackup); const newCount = countGalleries(galleries); if (newCount < originalCount) { console.error(`Items were lost! Original: ${originalCount}, New: ${newCount}`); restoreFromBackup(); return; } // Save changes if (window.saveGalleries) { window.saveGalleries().then(() => { console.log('Gallery structure saved successfully after drag'); }).catch(error => { console.error('Error saving gallery structure after drag:', error); }); } } catch (error) { console.error('Error updating gallery structure:', error); restoreFromBackup(); } } }); sortableInstances.push(sortable); } catch (error) { console.error(`Error initializing sortable for container ${i}:`, error); } } return sortableInstances; } // NEW HELPER FUNCTION: Gets the nesting level of an element function getElementNestingLevel(element) { let level = 0; let current = element; // Count how many nested-sortable parents this element has while (current && current.parentElement) { if (current.parentElement.classList && current.parentElement.classList.contains('nested-sortable')) { level++; } current = current.parentElement; } return level; } // NEW HELPER FUNCTION: Determines if an item would become nested function wouldBeNestedItem(draggedEl, relatedEl, event) { if (!relatedEl || !draggedEl) return false; // Get mouse position const mouseX = event.clientX; // Get the bounding rect of the related element const rect = relatedEl.getBoundingClientRect(); // Determine the nesting threshold - how far from the left edge // the mouse needs to be to consider it a nesting operation const nestingThreshold = 25; // pixels from left edge // Check if mouse is within the nesting threshold from the left edge const distanceFromLeft = mouseX - rect.left; // Check if we should nest (mouse is within threshold of left edge) const wouldNest = distanceFromLeft <= nestingThreshold; // Log the calculation for debugging console.log(`Nesting check: distance=${distanceFromLeft}px, threshold=${nestingThreshold}px, would nest=${wouldNest}`); return wouldNest; } /** * Helper function to find gallery by ID in a tree structure * @param {Array} galleries - The gallery array to search * @param {number} id - The ID to find * @returns {Object|null} - The found gallery or null */ function findGalleryById(galleries, id) { if (!Array.isArray(galleries)) return null; // First check at the current level const directMatch = galleries.find(g => g.id == id); if (directMatch) return directMatch; // Then check in children for (const gallery of galleries) { if (gallery.children && gallery.children.length > 0) { const childMatch = findGalleryById(gallery.children, id); if (childMatch) return childMatch; } } return null; } /** * Get all gallery IDs from a tree structure * @param {Array} galleries - Array of galleries * @returns {Array} - Flat array of all IDs */ function getAllGalleryIds(galleries) { if (!Array.isArray(galleries)) return []; let ids = []; galleries.forEach(gallery => { if (gallery && gallery.id) { ids.push(gallery.id); } // Add child IDs if any if (gallery.children && Array.isArray(gallery.children)) { ids = ids.concat(getAllGalleryIds(gallery.children)); } }); return ids; } /** * Enhanced count function that handles edge cases * @param {Array} galleryArray - The array to count * @returns {number} - The total count of galleries */ function countGalleries(galleryArray) { if (!Array.isArray(galleryArray)) return 0; return galleryArray.reduce((count, gallery) => { if (!gallery) return count; // Skip null/undefined items // Count this gallery let total = 1; // If it has children, count them too (recursively) if (gallery.children && Array.isArray(gallery.children)) { total += countGalleries(gallery.children); } return count + total; }, 0); } /** * Enhanced backup restoration function with better debugging and notification */ function restoreFromBackup() { console.warn('Restoring galleries from backup due to invalid operation'); // Verify we have a valid backup if (!galleriesBackup || !Array.isArray(galleriesBackup) || galleriesBackup.length === 0) { console.error('No valid backup available for restoration'); // Create a backup from the current DOM as a last resort try { console.log('Attempting to rebuild from DOM structure...'); rebuildGalleriesFromDOM(); return; } catch (rebuildError) { console.error('Failed to rebuild from DOM:', rebuildError); // Continue with normal user notification } } else { // Restore from backup galleries = JSON.parse(JSON.stringify(galleriesBackup)); } // Re-render the tree with the restored data renderGalleries(); // Force reinitialize nested sortables after restoration setTimeout(() => { initializeNestedSortables(); }, 300); } /** * Emergency recovery function that tries to rebuild galleries from DOM * This is a last resort when backup restoration fails */ function rebuildGalleriesFromDOM() { console.log('EMERGENCY RECOVERY: Attempting to rebuild galleries from DOM structure'); // Create a new galleries array const rebuiltGalleries = []; // This will track items we've already processed const processedIds = new Set(); // Function to process a container and its children function processContainer(container, parentId = null) { // Get all immediate list items const items = container.querySelectorAll(':scope > li'); items.forEach(item => { // Get item ID from data attribute const id = parseInt(item.dataset.id); if (isNaN(id)) return; // Skip if no valid ID // Skip already processed items to avoid duplicates if (processedIds.has(id)) return; processedIds.add(id); // Try to find the original gallery data let galleryData = null; // First check current galleries array const existingGallery = findGalleryById(galleries, id); if (existingGallery) { galleryData = { ...existingGallery }; delete galleryData.children; // We'll rebuild the hierarchy } // If not found, check if we have a backup else if (galleriesBackup && Array.isArray(galleriesBackup)) { const backupGallery = findGalleryById(galleriesBackup, id); if (backupGallery) { galleryData = { ...backupGallery }; delete galleryData.children; // We'll rebuild the hierarchy } } // If we still don't have data, create minimal placeholder if (!galleryData) { // Try to extract title from DOM const titleElement = item.querySelector('.menu-item'); const title = titleElement ? titleElement.textContent.trim() : `Item ${id}`; galleryData = { id: id, title: title, visible: !item.classList.contains('hidden-gallery') }; } // Update parent ID to match DOM structure galleryData.parentId = parentId; // Add to rebuilt galleries rebuiltGalleries.push(galleryData); // Process children if any const childContainer = item.querySelector('ol.nested-sortable'); if (childContainer) { processContainer(childContainer, id); } }); } // Start with root container const rootContainer = document.querySelector('#galleryTree > ol.nested-sortable'); if (rootContainer) { processContainer(rootContainer); // Replace global galleries array with our rebuilt one galleries = rebuiltGalleries; // Re-render with the rebuilt data renderGalleries(); console.log(`RECOVERY COMPLETE: Rebuilt ${rebuiltGalleries.length} items from DOM`); return true; } else { console.error('RECOVERY FAILED: Root container not found'); return false; } } // Make sure these functions are available to the global scope window.initializeNestedSortables = initializeNestedSortables; window.restoreFromBackup = restoreFromBackup; window.getElementNestingLevel = getElementNestingLevel; window.wouldBeNestedItem = wouldBeNestedItem; function hasDescendant(itemId, possibleDescendantId) { // Check if possibleDescendantId is a descendant of itemId const children = galleries.filter(g => g.parentId === itemId); if (children.some(child => child.id === possibleDescendantId)) { return true; } return children.some(child => hasDescendant(child.id, possibleDescendantId)); } function updateGalleryStructure() { // Create a new array to hold the updated gallery structure const newGalleries = []; // Function to recursively process nested lists function processNestedList(container, parentId = null) { if (!container || !container.children) { console.warn('Invalid container in processNestedList', container); return; } const items = container.children; Array.from(items).forEach(item => { if (item.tagName !== 'LI') return; // Skip non-list items const id = parseInt(item.dataset.id); if (isNaN(id)) { console.warn('Invalid ID in list item:', item); return; } const gallery = galleries.find(g => g.id === id); if (gallery) { // Create a new gallery object without children const newGallery = { ...gallery }; if (newGallery.children) delete newGallery.children; // Update parent ID newGallery.parentId = parentId; // Add to new galleries array newGalleries.push(newGallery); // Process any nested OL with class 'nested-sortable' within this item const nestedList = item.querySelector('ol.nested-sortable'); if (nestedList) { processNestedList(nestedList, id); } } else { console.warn(`Gallery with ID ${id} not found in galleries array`); } }); } // Start processing from the root list const rootList = document.querySelector('#galleryTree > ol.nested-sortable'); if (rootList) { try { processNestedList(rootList); // Verify that we haven't lost any galleries if (newGalleries.length < galleries.length) { console.error(`Gallery count mismatch! Original: ${galleries.length}, New: ${newGalleries.length}`); // Find missing galleries const originalIds = galleries.map(g => g.id); const newIds = newGalleries.map(g => g.id); const missingIds = originalIds.filter(id => !newIds.includes(id)); if (missingIds.length > 0) { console.error('Missing gallery IDs:', missingIds); // Add missing galleries to the new structure missingIds.forEach(id => { const missingGallery = galleries.find(g => g.id === id); if (missingGallery) { // Add it as a top-level item const newGallery = { ...missingGallery, parentId: null }; if (newGallery.children) delete newGallery.children; newGalleries.push(newGallery); console.log(`Rescued gallery: ${newGallery.title} (ID: ${newGallery.id})`); } }); } } // Update the galleries array with the new structure galleries = newGalleries; // Save the updated structure saveGalleries(); } catch (error) { console.error('Error in updateGalleryStructure:', error); throw error; // Re-throw to trigger the backup restore } } else { console.warn('Root list not found'); throw new Error('Root list not found'); // Throw error to trigger backup restore } } function toggleSubmenu(id, event) { // Ensure the event doesn't interfere with drag operations event.stopPropagation(); const listItem = document.querySelector(`li[data-id="${id}"]`); if (listItem) { // Toggle the expanded class listItem.classList.toggle('expanded'); // Find the submenu toggle icon and rotate it const toggleIcon = listItem.querySelector('.toggle-icon'); if (toggleIcon) { toggleIcon.classList.toggle('rotated'); } } } /** * Improved slugify function with robust character handling * @param {string} text - The text to convert to a slug * @returns {string} A URL-friendly slug */ function slugify(text) { // Guard against null or empty input if (!text || text.trim() === '') { return 'page-' + Date.now(); } // First convert to lowercase let slug = text.toLowerCase(); // Replace spaces with hyphens slug = slug.split(' ').join('-'); // Filter out unwanted characters (keep only a-z, 0-9, and hyphens) let filtered = ''; for (let i = 0; i < slug.length; i++) { const char = slug.charAt(i); if ((char >= 'a' && char <= 'z') || (char >= '0' && char <= '9') || char === '-') { filtered += char; } } slug = filtered; /* // Clean up multiple hyphens while (slug.indexOf('--') !== -1) { slug = slug.replace('--', '-'); } */ // Remove leading and trailing hyphens if (slug.startsWith('-')) { slug = slug.substring(1); } if (slug.endsWith('-')) { slug = slug.substring(0, slug.length - 1); } // If slug is empty after processing, use a default if (!slug || slug === '-') { return 'page-' + Date.now(); } return slug; } // Update the addPage function to use our new robust slugify function /** * Adds a new page to the galleries array * @returns {number|null} The new page ID or null if error */ function addPage() { try { // Get the page title const titleInput = document.getElementById('galleryTitle'); if (!titleInput) { console.error('Page title input not found'); return; } const title = titleInput.value.trim(); // Validate title is not empty if (!title) { alert('Please enter a page title'); return; } // Get parent ID with error handling const parentSelect = document.getElementById('galleryParent'); const parentId = parentSelect && parentSelect.value ? parseInt(parentSelect.value) : null; // Generate a unique ID for the page const id = Date.now(); const pageId = `page_${id}`; // Calculate slug using our more robust slugify function const pageSlug = slugify(title); // Create the page object with explicit properties const page = { id: id, title: title, isPage: true, isIntegrated: true, isSubmenu: false, pageId: pageId, visible: true, parentId: parentId, siteId: siteId, // Set slug and URL explicitly slug: pageSlug, url: '/' + pageSlug }; console.log(`Creating new page with ID: ${id}, title: "${title}"`); // Add to galleries array galleries.push(page); // CRITICAL: Ensure window.galleries is synchronized if (typeof window.galleries === 'undefined') { window.galleries = galleries; } else { // Make sure the page is in window.galleries too const existingIndex = window.galleries.findIndex(g => g.id === id); if (existingIndex >= 0) { window.galleries[existingIndex] = page; } else { window.galleries.push(page); } } // Initialize empty page elements array in PageManager if (window.PageManager) { window.PageManager.elements[pageId] = []; } // Update UI first renderGalleries(); clearAddForm(); toggleAddForm(); // Set as active page console.log(`Setting new page "${page.title}" as active`); activeGalleryId = id; window.activeGalleryId = id; // Update active states in the menu updateActiveStates(); // Save to server saveGalleries().then(() => { console.log(`Page "${page.title}" saved successfully`); // Double check that our gallery is still in the array const pageExists = galleries.some(g => g.id === id); console.log(`Page exists in galleries array: ${pageExists}`); if (pageExists) { // Load the new page after a slight delay to ensure DOM is updated setTimeout(() => { console.log(`Loading new page "${page.title}" with ID: ${id}`); loadPage(id); }, 50); } else { console.error(`Page with ID ${id} not found in galleries array after save`); } }).catch(error => { alert('Error saving page. Please try again.'); }); return id; } catch (error) { console.error('Error adding page:', error); alert('Error adding page. Please try again.'); return null; } } /** * Ensures a slug is unique among existing galleries * @param {string} slug - The base slug to check * @param {Array} existingGalleries - Array of existing gallery configs * @returns {string} A unique slug */ function ensureUniqueSlug(slug, existingGalleries) { // Guard against invalid slugs if (!slug || slug === '-') { slug = 'page-' + Date.now(); } let finalSlug = slug; let counter = 1; // Check if slug already exists while (existingGalleries.some(g => g.slug === finalSlug)) { finalSlug = `${slug}-${counter}`; counter++; } return finalSlug; } /** * Adds a new gallery based on the selected type * @returns {number|null} The new gallery ID or null if error */ function addGallery() { try { // Get the gallery title const titleInput = document.getElementById('galleryTitle'); if (!titleInput) { console.error('Gallery title input not found'); return; } const title = titleInput.value.trim(); // Get the selected gallery type const galleryTypeRadio = document.querySelector('input[name="galleryType"]:checked'); if (!galleryTypeRadio) { console.error('No gallery type selected'); alert('Please select a gallery type'); return; } const galleryType = galleryTypeRadio.value; console.log("Selected gallery type:", galleryType); // If this is a page, use the addPage function if (galleryType === 'page') { console.log("Creating page instead of gallery"); return addPage(); } // For spacer type, we don't require a title if (galleryType !== 'spacer' && !title) { alert('Please enter a gallery title'); return; } // Continue with gallery creation logic // Generate unique ID const id = Date.now(); // Get parent gallery if selected const parentSelect = document.getElementById('galleryParent'); const parentId = parentSelect.value ? parseInt(parentSelect.value) : null; // Get URL if this is an external gallery const galleryUrl = document.getElementById('galleryUrl'); const url = galleryTypeRadio.value === 'external' && galleryUrl ? galleryUrl.value.trim() : ""; // Set basic properties const gallery = { id: id, title: title || "Spacer", // Use "Spacer" as internal title if no title provided parentId: parentId, visible: true }; // Set type-specific properties if (galleryTypeRadio.value === 'spacer') { // Spacer properties gallery.isSpacer = true; gallery.title = title || "Spacer"; // For internal reference } else if (galleryTypeRadio.value === 'integrated') { // Generate pageId for integrated galleries gallery.isIntegrated = true; gallery.pageId = generatePageId(); // Generate and ensure unique slug const baseSlug = slugify(title); gallery.slug = ensureUniqueSlug(baseSlug, galleries); gallery.url = "/" + gallery.slug; // Get the siteId from multiple possible sources const siteId = window.Parameters?.siteId || window.siteId || document.querySelector('meta[name="hydra-site-id"]')?.getAttribute('content') || ''; // Add galleryOptions with GUID=siteId as the manualCollectionName gallery.galleryOptions = { manualCollectionName: `GUID=${siteId}`, isIntegratedGallery: true, pageId: gallery.pageId, // Store pageId for reference siteId: siteId }; } else if (galleryTypeRadio.value === 'folder') { gallery.isFolder = true; gallery.isCollapsed = true; // Make folders closed by default } else if (galleryTypeRadio.value === 'external') { // External URL gallery.url = url; gallery.isExternal = true; // Add this flag to identify external URLs } console.log(`Creating new gallery with ID: ${id}, title: "${title}"`); // Add to galleries array galleries.push(gallery); // CRITICAL: Ensure window.galleries is synchronized if (typeof window.galleries === 'undefined') { window.galleries = galleries; } else { // Make sure the gallery is in window.galleries too const existingIndex = window.galleries.findIndex(g => g.id === id); if (existingIndex >= 0) { window.galleries[existingIndex] = gallery; } else { window.galleries.push(gallery); } } // Update UI first renderGalleries(); clearAddForm(); toggleAddForm(); // For non-folder types, activate and load the new gallery immediately if (!gallery.isFolder && !gallery.isSubmenu && !gallery.isSpacer) { console.log(`Setting new gallery "${gallery.title}" as active`); // Set as active gallery activeGalleryId = id; window.activeGalleryId = id; // Update active states in the menu updateActiveStates(); } // Save galleries after UI update saveGalleries().then(() => { console.log(`Gallery "${gallery.title}" saved successfully`); // Double check that our gallery is still in the array const galleryExists = galleries.some(g => g.id === id); console.log(`Gallery exists in galleries array: ${galleryExists}`); // For non-folder types, load the new gallery content after a slight delay if (!gallery.isFolder && !gallery.isSubmenu && !gallery.isSpacer && gallery.visible !== false) { setTimeout(() => { console.log(`Loading new gallery "${gallery.title}" with ID: ${id}`); if (gallery.isIntegrated) { loadGallery(id); } else if (gallery.isExternal && gallery.url) { // For external URLs, set the iframe src const frame = document.getElementById('galleryFrame'); if (frame) frame.src = gallery.url; } }, 50); } }).catch(error => { console.error(`Error saving gallery "${gallery.title}":`, error); alert('Error saving gallery. Please try again.'); }); // Return the new gallery ID return id; } catch (error) { console.error('Error adding gallery:', error); alert('Error adding gallery. Please try again.'); } } function debugDOMState(message) { console.log(`[DEBUG] ${message}`); const galleryContainer = document.querySelector('.gallery-container'); const iframe = document.getElementById('galleryFrame'); const pageContainers = document.querySelectorAll('.page-container'); console.log(`- Gallery container: ${galleryContainer ? 'found' : 'not found'}`); console.log(`- iframe: ${iframe ? 'found' : 'not found'}, display: ${iframe?.style.display}`); console.log(`- Page containers: ${pageContainers.length} found`); pageContainers.forEach((container, index) => { console.log(` - Container ${index}: display: ${container.style.display}, visibility: ${container.style.visibility}`); }); } /** * Global helper function to check if we're in edit mode * This centralizes the logic for detecting edit mode across all components */ window.isInEditMode = function() { // Method 1: Check global isEditing variable if (typeof window.isEditing === 'boolean') { return window.isEditing; } // Method 2: Check sidebar editing class const sidebar = document.querySelector('.sidebar'); if (sidebar && sidebar.classList.contains('editing')) { return true; } // Method 3: Check Parameters.isInEditor if (window.Parameters && typeof window.Parameters.isInEditor === 'boolean') { return window.Parameters.isInEditor; } // Method 4: Check if any page containers have editing class const editingContainers = document.querySelectorAll('.page-container.editing'); if (editingContainers && editingContainers.length > 0) { return true; } // Default to false if no indicators found return false; }; /** * This function synchronizes edit mode state across all components */ window.updateGlobalEditState = function(isEditing) { console.log('Updating global edit state:', isEditing); // Update global variable window.isEditing = isEditing; // Update Parameters object if it exists if (window.Parameters) { window.Parameters.isInEditor = isEditing; } // Update sidebar class const sidebar = document.querySelector('.sidebar'); if (sidebar) { if (isEditing) { sidebar.classList.add('editing'); } else { sidebar.classList.remove('editing'); } } // Update page containers const pageContainers = document.querySelectorAll('.page-container'); pageContainers.forEach(container => { if (isEditing) { container.classList.add('editing'); } else { container.classList.remove('editing'); } }); // Notify all components via event document.dispatchEvent(new CustomEvent('edit-mode-changed', { detail: { editing: isEditing } })); }; /** * loadPage function that safely handles undefined galleries * @param {number} id - The gallery ID * @param {Event} event - Optional event object */ function loadPage(id, event){ if (event) { event.preventDefault(); event.stopPropagation(); const now = Date.now(); const lastCallTime = window._lastPageLoadTime || 0; window._lastPageLoadTime = now; if (now - lastCallTime < 100) { console.log('Ignoring duplicate loadPage call'); return; } } console.log('loadPage: Loading page with gallery ID:', id); stopAutoAdvanceTimer(); let galleriesData = window.galleries || galleries; const gallery = findGalleryById(galleriesData, id); if (!gallery) { console.warn('No gallery found with ID:', id, 'for page load.'); return; } if (typeof window.removeGalleryScriptsWithPause === 'function') { window.removeGalleryScriptsWithPause(); } window.activeGalleryId = id; activeGalleryId = id; const galleryContainer = document.querySelector('.gallery-container'); if (galleryContainer) { galleryContainer.innerHTML = ''; } if (!gallery.pageId) gallery.pageId = `page_${id}`; if (!gallery.isPage) gallery.isPage = true; if (!window._pageIdToGalleryId) window._pageIdToGalleryId = {}; window._pageIdToGalleryId[gallery.pageId] = id; if (gallery.pageElements && Array.isArray(gallery.pageElements)) { if (window.PageManager && window.PageManager.elements) { window.PageManager.elements[gallery.pageId] = [...gallery.pageElements]; } } else if (window.PageManager && window.PageManager.elements && window.PageManager.elements[gallery.pageId] && window.PageManager.elements[gallery.pageId].length > 0) { gallery.pageElements = [...window.PageManager.elements[gallery.pageId]]; } if (window.PageManager && typeof window.PageManager.loadPage === 'function') { try { window.PageManager.loadPage(gallery.pageId); const isCurrentlyEditing = typeof isInEditMode === 'function' ? isInEditMode() : false; if (isCurrentlyEditing && typeof window.PageManager.setEditMode === 'function') { window.PageManager.setEditMode(isCurrentlyEditing); } } catch (error) { console.error('Error loading page with PageManager:', error); } } else { console.error('PageManager not found or loadPage method not available'); } // Apply menu visibility const bodyEl = document.body; if (gallery.hideMenuOnPage && !isInEditMode()) { // Check if not in edit mode bodyEl.classList.add('menu-hidden-on-page'); } else { bodyEl.classList.remove('menu-hidden-on-page'); // Ensure menu is visible if in edit mode or not hidden } if (typeof updateActiveStates === 'function') updateActiveStates(); if (typeof updateMobileTitle === 'function') updateMobileTitle(); if (typeof closeMobileMenu === 'function') closeMobileMenu(); // Close the gallery options panel when navigating to a different page if (typeof window.closeOptionsPanel === 'function') { window.closeOptionsPanel(); } if (typeof updateURLWithGallerySlug === 'function') updateURLWithGallerySlug(gallery); } function generatePageId() { const characters = 'abcdefghijklmnopqrstuvwxyz0123456789'; let result = ''; const charactersLength = characters.length; for (let i = 0; i < 8; i++) { result += characters.charAt(Math.floor(Math.random() * charactersLength)); } return result; }; function normalizeNameForComparison(name) { if (!name) { console.log("Empty name passed to normalization"); return ''; } console.log(`Normalizing name: "${name}"`); // Step 1: Convert to lowercase let normalized = name.toLowerCase(); console.log(`After lowercase: "${normalized}"`); // Step 2: Replace spaces with hyphens (without regex) normalized = normalized.split(' ').join('-'); console.log(`After space replacement: "${normalized}"`); // Step 3: Filter out unwanted characters (without regex) let filtered = ''; for (let i = 0; i < normalized.length; i++) { const char = normalized.charAt(i); // Keep only a-z, 0-9, and hyphens if ((char >= 'a' && char <= 'z') || (char >= '0' && char <= '9') || char === '-') { filtered += char; } } normalized = filtered; console.log(`After character filtering: "${normalized}"`); // Step 4: Clean up multiple hyphens (without regex) while (normalized.indexOf('--') !== -1) { normalized = normalized.split('--').join('-'); } // Step 5: Remove leading and trailing hyphens (without regex) if (normalized.charAt(0) === '-') { normalized = normalized.substring(1); } if (normalized.charAt(normalized.length - 1) === '-') { normalized = normalized.substring(0, normalized.length - 1); } // Check if the result is valid if (!normalized || normalized === '-') { console.log(`WARNING: Normalization produced invalid result: "${normalized}"`); } else { console.log(`Final normalized slug: "${normalized}"`); } return normalized; } function ensureUniqueSlug(slug, existingGalleries) { if (!slug) return 'gallery'; let finalSlug = slug; let counter = 1; // Check if slug exists in any gallery const slugExists = function(s) { for (let i = 0; i < existingGalleries.length; i++) { if (existingGalleries[i].slug === s) { return true; } } return false; }; // Keep incrementing counter until unique while (slugExists(finalSlug)) { finalSlug = slug + '-' + counter; counter++; } return finalSlug; } /** * Gathers the current complete site configuration from the client-side state * and triggers a download of the data as a single JSON file. * This provides a simple way to back up or export the current site structure. */ function downloadCurrentSiteJSON() { try { console.log('Gathering current site data for download...'); // Get the current hostname to use in the filename. const hostname = window.location.hostname.replace('www.', ''); // --- FIX: Ensure we get the most up-to-date menu styles --- // The MenuStyleCustomizer.settings object is the most reliable source // for the current styles being used on the client. const currentMenuStyles = window.MenuStyleCustomizer?.settings || {}; // Get current galleries const currentGalleries = window.galleries || galleries || []; // --- STREAMLINED: Process galleries without including image data --- const enhancedGalleries = currentGalleries.map(gallery => { // If this is a Classic collection gallery, add note about manual retrieval if (gallery.galleryOptions?.manualCollectionName?.startsWith('GUID=')) { const classicGuid = gallery.galleryOptions.manualCollectionName.substring(5); console.log(`Processing Classic collection gallery: ${gallery.title} (GUID: ${classicGuid})`); return { ...gallery, _note: `This gallery references Classic collection GUID=${classicGuid}. To get the full image data with captions, you may need to manually fetch the Classic collection JSON from: https://storage.neonsky.app/sites:site_${classicGuid}.json`, _classicGuid: classicGuid, _requiresClassicData: true }; } return gallery; }); // Assemble the complete site configuration object from global state variables. // This structure mirrors what the `save-config` API endpoint expects, ensuring // the exported file is a complete and re-importable replica. const siteData = { // The core menu/page structure with enhanced Classic collection data. galleries: enhancedGalleries, // Admin emails are managed server-side for security. We export the list if it's // available on the client from a previous check, but the server remains the // source of truth. A re-import will not overwrite the admin list. adminEmails: window.siteConfig?.adminEmails || [], // The unique ID for the site. siteId: window.siteId || window.Parameters?.siteId || '', // Custom styling rules for the menu. menuStyles: currentMenuStyles, // All elements configured to appear in the sidebar. sidebarElements: window.SidebarManager?.elements || [], // Global site metadata for SEO and analytics. siteMetadata: window.siteMetadata || {}, // Add metadata about Classic collections that need manual data retrieval _classicCollections: enhancedGalleries .filter(g => g._requiresClassicData) .map(g => ({ title: g.title, guid: g._classicGuid, note: g._note })) }; console.log('Enhanced site data collected for export:', siteData); // Convert the JavaScript object to a formatted JSON string. const jsonString = JSON.stringify(siteData, null, 2); // Create a Blob, which is a file-like object of immutable, raw data. const blob = new Blob([jsonString], { type: 'application/json' }); // Create a temporary link element to trigger the browser's download functionality. const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${hostname}-config.json`; // e.g., 'yoursite.com-config.json' document.body.appendChild(a); a.click(); // Clean up by removing the temporary link and revoking the object URL to free up memory. document.body.removeChild(a); URL.revokeObjectURL(url); console.log('Enhanced JSON download initiated successfully.'); // Show a helpful message about Classic collections const classicCount = siteData._classicCollections?.length || 0; if (classicCount > 0) { console.log(`Export Summary: ${classicCount} Classic collection(s) detected. The exported JSON includes gallery structure and settings, but for full image data with captions, you may need to manually fetch the Classic collection JSON files.`); } } catch (error) { console.error('Error creating JSON download:', error); alert('An error occurred while preparing the download. Please check the console.'); } } // Build Hydra Classic-style NDJSON (first line: metadata; second line: imageMetadata array) function buildHydraClassicNdjson(classicGuid, images) { const header = { title: 'Untitled', description: '', version: '1', isClassic: true, isClassicCollection: true, classicGuid: 'GUID=' + String(classicGuid || '') }; const imageArray = Array.isArray(images) ? images.map(function(img, idx) { var path = ''; if (img) { path = img.image || img.link || img.originalUrl || (img.urls && img.urls.original) || ''; } // Derive filename base from any provided path/filename and normalize extension to .jpg var fnameBase = ''; if (typeof path === 'string' && path.length > 0) { var slash = path.lastIndexOf('/'); var raw = slash >= 0 ? path.substring(slash + 1) : path; // remove query/hash var q = raw.indexOf('?'); if (q >= 0) raw = raw.substring(0, q); var h = raw.indexOf('#'); if (h >= 0) raw = raw.substring(0, h); // strip extension if present var dot = raw.lastIndexOf('.'); fnameBase = dot > 0 ? raw.substring(0, dot) : raw; } else if (img && typeof img.filename === 'string') { var rf = img.filename; var q2 = rf.indexOf('?'); if (q2 >= 0) rf = rf.substring(0, q2); var h2 = rf.indexOf('#'); if (h2 >= 0) rf = rf.substring(0, h2); var d2 = rf.lastIndexOf('.'); fnameBase = d2 > 0 ? rf.substring(0, d2) : rf; } var filename = fnameBase ? (fnameBase + '.jpg') : ''; // Build CDN image URL using site GUID: https://cdn.neonsky.app/{siteGuid}/images/{filename} // Note: classicGuid is a site GUID, not an image GUID - it identifies the site, and images are stored under that site's path var guidRaw = String(classicGuid || '').replace(/^GUID=/, ''); var cdnImageUrl = filename && guidRaw ? ('https://cdn.neonsky.app/' + guidRaw + '/images/' + filename) : path; var rec = { caption: (img && (img.caption || img.description)) || '', description: (img && (img.description || img.caption)) || '', isPreview: idx === 1, title: (img && img.title) || '', 'alt-text': (img && (img['alt-text'] || img.altText || '')) || '', image: cdnImageUrl || '', link: '', 'link-text': '', filename: filename, isClassic: true, isClassicCollection: true, classicGuid: 'GUID=' + String(classicGuid || '') }; return rec; }) : []; // Build NDJSON as: header line, imageMetadata array line, then one line per full image record var body = { imageMetadata: imageArray }; var out = [JSON.stringify(header), JSON.stringify(body)]; for (var k = 0; k < imageArray.length; k++) { out.push(JSON.stringify(imageArray[k])); } return out.join('\n') + '\n'; } async function uploadNdjsonToTigris(siteId, pageId, ndjsonContent) { // Dual upload strategy: Fly.io (uncompressed) + Irys (compressed if <800KB) const fileName = String(siteId || '') + '_' + String(pageId || '') + '.ndjson'; const flyUrl = 'https://hydra-press-v2.fly.dev/publish'; const irysUrl = 'https://hydra-press-v2.fly.dev/upload-irys'; console.error('Starting dual NDJSON upload for:', fileName); console.error('Content length:', ndjsonContent.length, 'characters'); // COMPRESSION: Compress for Irys only (not Tigris) console.error('🔄 Compressing NDJSON with gzip for Irys...'); const encoder = new TextEncoder(); const ndjsonBytes = encoder.encode(ndjsonContent); const compressedStream = new Response(ndjsonBytes).body.pipeThrough(new CompressionStream('gzip')); const compressedBlob = await new Response(compressedStream).blob(); const compressedBytes = new Uint8Array(await compressedBlob.arrayBuffer()); const originalSize = ndjsonBytes.length; const compressedSize = compressedBytes.length; const compressionRatio = ((1 - compressedSize / originalSize) * 100).toFixed(2); console.error('📊 Compression results:', { original: originalSize + ' bytes (' + (originalSize / 1024).toFixed(2) + ' KB)', compressed: compressedSize + ' bytes (' + (compressedSize / 1024).toFixed(2) + ' KB)', ratio: compressionRatio + '%' }); const IRYS_SIZE_LIMIT = 800 * 1024; // 800KB limit for compressed Irys upload const shouldUploadToIrys = compressedSize < IRYS_SIZE_LIMIT; console.error('Irys upload eligible: ' + shouldUploadToIrys + ' (compressed: ' + (compressedSize / 1024).toFixed(2) + ' KB)'); // STEP 1: Upload to Irys (non-blocking, compressed if <800KB) let irysResult = null; if (shouldUploadToIrys) { try { console.error('🔄 Starting Irys upload (compressed, non-blocking)...'); // Convert compressed bytes to base64 const compressedBase64 = btoa(String.fromCharCode(...compressedBytes)); const irysResp = await fetch(irysUrl, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ compressedData: compressedBase64, originalSize: originalSize, compressedSize: compressedSize, siteId: siteId, pageId: pageId }) }); if (irysResp.ok) { irysResult = await irysResp.json(); console.error('✅ Irys upload successful:', irysResult.txId); console.error(' Irys Gateway:', irysResult.urls?.irys); console.error(' ar.io Gateway:', irysResult.urls?.arweave); // Log wallet balance and cost if (irysResult.walletBalance !== undefined) { console.error('💰 Wallet Balance:', irysResult.walletBalance.toFixed(6), 'AR'); } if (irysResult.uploadCost !== undefined) { const costUSD = (irysResult.uploadCost * 20).toFixed(4); if (irysResult.uploadCost === 0) { console.error('💸 Upload Cost: FREE (0 AR)'); } else { console.error('💸 Upload Cost:', irysResult.uploadCost.toFixed(6), 'AR (~$' + costUSD + ' USD at $20/AR)'); } } } else { const errorText = await irysResp.text().catch(() => 'Unknown error'); console.warn('⚠️ Irys upload failed (non-critical):', irysResp.status, errorText); } } catch (irysError) { console.warn('⚠️ Irys upload error (non-critical):', irysError.message); // Continue - Irys failure doesn't block Fly.io upload } } else { console.error('⏭️ Skipping Irys upload (compressed size exceeds 800KB limit)'); } // STEP 2: Upload to Fly.io (CRITICAL - must succeed, uncompressed) try { console.error('🔄 Starting Fly.io upload (uncompressed)...'); const flyResp = await fetch(flyUrl, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ ndjsonContent, dataFileName: fileName }) }); console.log('Server response status:', flyResp.status); if (!flyResp.ok) { const errorText = await flyResp.text().catch(() => 'Unknown error'); console.error('❌ Fly.io upload failed', flyResp.status, 'for', fileName + ':', errorText); return { success: false, irysResult: null }; } const flyData = await flyResp.json().catch(() => null); if (flyData && flyData.urls) { console.log('✅ Fly.io upload successful:', flyData.urls); console.log(' Tigris URL:', flyData.urls.urlTigris); // Return comprehensive result const result = { success: true, flyUrls: flyData.urls, irysResult: irysResult, uploadedTo: irysResult ? ['fly', 'irys'] : ['fly'], fileSize: compressedSize, // Irys compressed size originalFileSize: originalSize // Original uncompressed size }; console.log('📦 Upload complete:', { destinations: result.uploadedTo.join(' + '), txId: irysResult?.txId || 'N/A', flyUrl: flyData.urls.urlTigris }); return result; } else { console.warn('Fly.io upload response missing URLs:', flyData); return { success: false, irysResult: null }; } } catch (flyError) { console.error('❌ Fly.io upload error for', fileName + ':', flyError); return { success: false, irysResult: null }; } } async function importClassicGalleries() { // Browser-scoped helper: mirrors server-side behavior with escaped patterns for template-safe JS function cleanDescriptionLeadingLines(text) { if (!text || typeof text !== 'string') return text; let result = text; // Remove leading empty TEXTFORMAT blocks:

result = result.replace( new RegExp( '^(\s*\]*\>\s*\]*\>\s*\]*\>\s*\<\/FONT\>\s*\<\/P\>\s*\<\/TEXTFORMAT\>\s*)+', 'i' ), '' ); // Remove leading empty P tags result = result.replace( new RegExp('^(\s*\]*\>\s*\<\/P\>\s*)+', 'i'), '' ); // Remove leading empty FONT tags result = result.replace( new RegExp('^(\s*\]*\>\s*\<\/FONT\>\s*)+', 'i'), '' ); // Remove leading BR tags result = result.replace( new RegExp('^(\s*\\s*)+', 'i'), '' ); // Remove any remaining leading whitespace return result.replace(new RegExp('^\s+', 'i'), '').trim(); } // Browser-scoped helper: fix multi-line links (template-safe escaped regex) function fixMultilineLinks(text) { if (!text || typeof text !== 'string') return text; return text.replace( new RegExp(']*>.*?Click.*?<\/a>]*>.*?HERE.*?<\/a>]*>.*?to Add to Cart.*?<\/a>', 'gis'), function (match) { const first = match.match(new RegExp('Click HERE to Add to Cart'; } return match; } ); } // Browser-scoped helper: convert plain text link phrase to HTML link function convertPlainTextLinks(text) { if (!text || typeof text !== 'string') return text; return text.replace( new RegExp('Click HERE to Add to Cart', 'g'), 'Click HERE to Add to Cart' ); } // Get input elements, including the new checkbox const guidInputEl = document.getElementById('classicGuid'); const pastedJsonEl = document.getElementById('pastedJson'); const parentIdInputEl = document.getElementById('importParent'); const createSubmenuCheckboxEl = document.getElementById('createSubmenu'); const submenuTitleInputEl = document.getElementById('submenuTitle'); const replaceMenuCheckboxEl = document.getElementById('replaceMenuCheckbox'); // Get values from inputs const guidInput = guidInputEl instanceof HTMLInputElement ? guidInputEl.value.trim() : ''; const pastedJson = pastedJsonEl instanceof HTMLTextAreaElement ? pastedJsonEl.value.trim() : ''; const replaceMenu = replaceMenuCheckboxEl instanceof HTMLInputElement ? replaceMenuCheckboxEl.checked : false; let parentId = parentIdInputEl instanceof HTMLSelectElement && parentIdInputEl.value ? parseInt(parentIdInputEl.value) : null; let createSubmenu = createSubmenuCheckboxEl instanceof HTMLInputElement ? createSubmenuCheckboxEl.checked : false; let submenuTitle = submenuTitleInputEl instanceof HTMLInputElement ? submenuTitleInputEl.value.trim() : ''; // If replacing the menu, ignore parent/submenu settings if (replaceMenu) { parentId = null; createSubmenu = false; } // Validate submenu title if checkbox is checked if (createSubmenu && !submenuTitle) { alert('Please provide a title for the submenu'); return; } // Show status display const statusEl = document.getElementById('importStatus'); const progressBarEl = statusEl ? statusEl.querySelector('.progress-bar') : null; const statusMessageEl = statusEl ? statusEl.querySelector('.status-message') : null; const progressBar = progressBarEl instanceof HTMLElement ? progressBarEl : null; const statusMessage = statusMessageEl instanceof HTMLElement ? statusMessageEl : null; if (statusEl && progressBar && statusMessage) { statusEl.style.display = 'block'; statusEl.classList.add('importing'); progressBar.style.width = '10%'; statusMessage.textContent = 'Preparing to import...'; } try { let classicData; let classicGuid; // Will hold the primary GUID for image path lookups // --- LOGIC TO GET DATA: Prioritize Pasted JSON --- if (pastedJson) { if (statusMessage) statusMessage.textContent = 'Parsing pasted JSON...'; classicData = JSON.parse(pastedJson); if (!classicData.galleries || !Array.isArray(classicData.galleries)) { throw new Error('Pasted JSON is invalid: "galleries" array not found.'); } // Use the first available GUID from the pasted data as the default for images classicGuid = classicData.galleries[0]?.classicGuid || classicData.siteId; } else if (guidInput) { // Check if this is the new import_ format if (guidInput.startsWith('import_')) { if (statusMessage) statusMessage.textContent = 'Fetching data via import_ format...'; const importGuid = guidInput.substring(7); // Remove 'import_' prefix const jsonUrl = `https://hydra.neonsky.app/json-imports/${guidInput}.json`; const response = await fetch(jsonUrl); if (!response.ok) throw new Error(`HTTP error! status: ${response.status} for ${jsonUrl}`); classicData = await response.json(); if (!classicData.galleries || !Array.isArray(classicData.galleries)) { throw new Error('Fetched import JSON is invalid: "galleries" array not found.'); } // HYBRID APPROACH: Also fetch the original GUID JSON for image metadata if (statusMessage) statusMessage.textContent = 'Fetching image metadata from original JSON...'; const originalJsonUrl = `https://cdn.neonsky.app/sites:site_${importGuid}.json`; try { const originalResponse = await fetch(originalJsonUrl); if (originalResponse.ok) { const originalData = await originalResponse.json(); // Get the default language from site configuration let defaultLanguageID = 'en'; // fallback if (originalData.languages && Array.isArray(originalData.languages)) { const defaultLang = originalData.languages.find(lang => lang.isDefault === true); if (defaultLang) { defaultLanguageID = defaultLang.id; } } // Merge image metadata from original JSON into import JSON galleries if (originalData.galleries && Array.isArray(originalData.galleries)) { classicData.galleries = classicData.galleries.map(importGallery => { // Find matching gallery in original data by name or categoryID const originalGallery = originalData.galleries.find(orig => orig.title === importGallery.title || // Primary match by title orig.name === importGallery.title || // Fallback: orig.name matches import.title orig.categoryID === importGallery.categoryID // Fallback: categoryID match ); if (originalGallery && originalGallery.images && Array.isArray(originalGallery.images)) { // Process all images from original gallery with metadata extraction const processedImages = originalGallery.images.map(originalImage => { // Apply EXACT same metadata processing logic as Edition Publisher let title = originalImage.title || ''; let dateline = originalImage.dateline || ''; let caption = originalImage.caption || ''; // Check languageVersions array for metadata (this is where most data is stored) if (originalImage.languageVersions && originalImage.languageVersions.length > 0) { // Look for site's default language first, then fall back to first available let langData = originalImage.languageVersions.find(lang => lang.languageID === defaultLanguageID); if (!langData) { langData = originalImage.languageVersions[0]; } // For language-enabled JSON files, prioritize languageVersions data // Only fall back to top-level if languageVersions data is empty if (!title || title.trim() === '') title = langData.title || ''; if (!dateline || dateline.trim() === '') dateline = langData.dateline || ''; if (!caption || caption.trim() === '') caption = langData.caption || ''; } // Convert Flash TEXTFORMAT to HTML (properly escaped for template literals) let description = caption; if (caption && caption.includes('TEXTFORMAT')) { try { // Use properly escaped HTML characters for template literal context description = caption .replace(/\]*\>/g, '') .replace(/\<\/TEXTFORMAT\>/g, '') .replace(/\]*\>/g, '\') .replace(/\<\/P\>/g, '\<\/p\>') .replace(/\]*\>/g, '') .replace(/\<\/FONT\>/g, '') .replace(/\]*\>/g, '\') .replace(/\<\/A\>/g, '\<\/a\>') .replace(/\/g, '\') .replace(/\<\/U\>/g, '\<\/u\>') .replace(/\/g, '\') .replace(/\<\/B\>/g, '\<\/strong\>') .replace(/\/g, '\') .replace(/\<\/I\>/g, '\<\/em\>') .replace(/"/g, '"') .replace(/'/g, "'") .replace(/\s+/g, ' ') .trim(); } catch (error) { console.warn('TEXTFORMAT conversion failed, using original caption:', error); description = caption; } } // Prepend dateline to description if it exists (convert to uppercase) if (dateline) { const uppercaseDateline = dateline.toUpperCase(); description = `${uppercaseDateline} ${description}`.trim(); } // Apply description fixes description = cleanDescriptionLeadingLines(description); description = fixMultilineLinks(description); description = convertPlainTextLinks(description); const imageEntry = { // Match EXACT structure from Edition Publisher hydraImageEntry image: originalImage.image || originalImage.urls?.original || '', caption: description, // Use processed description as caption description: description, // ALSO set description field (like Edition Publisher) title: title, 'alt-text': title, // Use title as alt-text like Edition Publisher link: originalImage.urls?.original || '', 'link-text': originalImage.urls?.original || '', isPreview: false, filename: originalImage.filename || '', originalUrl: originalImage.image || originalImage.urls?.original || '', dateline: dateline }; return imageEntry; }); return { ...importGallery, // Keep import gallery structure (order, settings, etc.) images: processedImages // Replace with processed original images }; } return importGallery; }); } } else { console.warn('Could not fetch original JSON for metadata, using import data only'); } } catch (error) { console.warn('Error fetching original JSON for metadata:', error); } // Extract original GUID for image path lookups // If importGuid has a suffix (e.g., "4bd5ebf7bda9d-all"), extract the base GUID // GUIDs are typically 13 characters, so split on common separators let baseGuid = importGuid; const guidMatch = importGuid.match(/^([a-f0-9]{13})(?:-.*)?$/i); if (guidMatch) { baseGuid = guidMatch[1]; } else { // Fallback: try to extract first 13-character hex string const hexMatch = importGuid.match(/^([a-f0-9]{13})/i); if (hexMatch) { baseGuid = hexMatch[1]; } } // Use the base GUID for image path lookups (e.g., /images/filename.jpg) classicGuid = baseGuid; console.log('Successfully loaded hybrid import data with import GUID:', importGuid); console.log('Using base GUID for image paths:', classicGuid); } else { // Standard GUID format - existing logic if (statusMessage) statusMessage.textContent = 'Fetching data via GUID...'; classicGuid = guidInput.startsWith('GUID=') ? guidInput.substring(5) : guidInput; console.log('Detected standard GUID format, GUID:', classicGuid); const jsonUrl = `https://storage.neonsky.app/sites:site_${classicGuid}.json`; console.log('Fetching classic JSON from:', jsonUrl); const response = await fetch(jsonUrl); if (!response.ok) throw new Error(`HTTP error! status: ${response.status} for ${jsonUrl}`); classicData = await response.json(); if (!classicData.galleries || !Array.isArray(classicData.galleries)) { throw new Error('Fetched JSON is invalid: "galleries" array not found.'); } console.log('Successfully loaded classic data with GUID:', classicGuid); } } else { alert('Please either paste site JSON or enter a Classic GUID to import.'); if (statusEl) statusEl.style.display = 'none'; return; } const galleriesForImport = classicData.galleries; if (statusMessage) statusMessage.textContent = `Found ${galleriesForImport.length} items to import...`; // --- HIERARCHY RECONSTRUCTION LOGIC --- const currentSiteId = window.Parameters?.siteId || siteId; let submenuParentId = parentId; const tempNewItems = []; const originalIdToNewIdMap = {}; // Create the optional top-level submenu if requested if (createSubmenu) { const submenuItem = { id: Date.now(), title: submenuTitle, isSubmenu: true, isCollapsed: true, visible: true, parentId: parentId, siteId: currentSiteId, children: [] }; galleries.push(submenuItem); submenuParentId = submenuItem.id; } // --- Pass 1: Create new items with correct data and map old IDs to new IDs --- for (let index = 0; index < galleriesForImport.length; index++) { const classicItem = galleriesForImport[index]; const newItemId = Date.now() + index + 100; originalIdToNewIdMap[classicItem.id] = newItemId; if (progressBar) progressBar.style.width = `${40 + Math.round(index * 50 / galleriesForImport.length)}%`; if (statusMessage) statusMessage.textContent = `Processing item ${index + 1} of ${galleriesForImport.length}...`; const title = classicItem.title || classicItem.languageVersions?.[0]?.title || classicItem.name || `Item ${classicItem.categoryID}`; let slug; // If pasting from JSON, *always* regenerate the slug from the title to ensure it's clean. // Otherwise (for GUID import), trust the existing slug if it's there. if (pastedJson) { slug = normalizeNameForComparison(title); } else if (classicItem.slug && classicItem.slug.trim() !== '') { slug = classicItem.slug; } else { slug = normalizeNameForComparison(title); } const finalSlug = ensureUniqueSlug(slug || 'gallery', [...galleries, ...tempNewItems]); // Ensure uniqueness // Determine the GUID to use for this specific item's image paths const itemGuid = classicItem.classicGuid || classicGuid; let newItem; // --- Check if it's an External URL --- if (classicItem.isExternal === true && classicItem.url) { console.log(`Creating external URL menu item: "${title}" -> ${classicItem.url}`); newItem = { id: newItemId, title, url: classicItem.url, isExternal: true, visible: classicItem.visible !== false, parentId: null, siteId: currentSiteId, slug: finalSlug, importSource: 'external-url' }; } // --- Check if it's a Page and build its elements --- else if (classicItem.isPage === true || classicItem.type === 3 || classicItem.behaviorID === 3) { const pageUniqueId = `page_${newItemId}`; let pageElements = []; // ================================================================= // ENHANCED: Handle pageElements for both pasted JSON and import_guid.json format // ================================================================= if (classicItem.pageElements && Array.isArray(classicItem.pageElements)) { console.log(`Using existing pageElements for page: "${title}"`); // Check if we have both pageElements and images - create two-column layout if (classicItem.images && classicItem.images.length > 0) { console.log(`Creating two-column layout for page: "${title}" with ${classicItem.images.length} images`); // Create metadata element pageElements.push({ id: newItemId + 1000, type: "metadata", title: "Metadata", visible: true, position: 0, metaTitle: classicItem.meta?.title || title, metaDescription: classicItem.meta?.description || "", metaKeywords: classicItem.meta?.keywords || "" }); // Create two-column container const leftColumnElements = []; const rightColumnElements = []; // Add images to left column const slides = classicItem.images.map((img) => { const imageFilename = (img.filename || `image_${Date.now()}`) + (img.metadata?.extension || '.jpg'); const imageKey = `${itemGuid}/images/${imageFilename}`; const imageUrl = `https://storage.neonsky.app/${imageKey}`; return { imageUrl, imageKey, imageFilename, imageType: img.metadata?.mimetype || 'image/jpeg', caption: cleanDescriptionLeadingLines(fixMultilineLinks(convertPlainTextLinks(img.caption || ''))), title: img.title || '', byline: img.byline || '', dateline: img.dateline || '', altText: img.metadata?.alt_text || '' }; }); leftColumnElements.push({ id: newItemId + 3100, type: "slideshow", title: "Page Images", visible: true, position: 0, slides, slideDuration: 5000, transitionDuration: 500, slideshowWidth: 88, slideshowHeight: 50, showFullImages: true }); // Add text content to right column classicItem.pageElements.forEach((el, elIndex) => { if (el.type === 'text' && el.content) { rightColumnElements.push({ id: newItemId + 3200 + elIndex, type: "text", title: "Text Block", visible: true, position: elIndex, textContent: el.content, textWidth: 70 }); } }); const columns = [ { id: newItemId + 3300, hAlign: "right", vAlign: "middle", elements: leftColumnElements }, { id: newItemId + 3400, hAlign: "left", vAlign: "middle", elements: rightColumnElements } ]; pageElements.push({ id: newItemId + 3000, type: "column-container", title: "Two Column Layout", visible: true, position: 1, columns, backgroundSlideshow: { enabled: false, slides: [], slideDuration: 5000, transitionDuration: 500, slideshowHeight: 50, showFullImages: false } }); } else { // No images, just transform the pageElements as before pageElements = classicItem.pageElements.map((el, elIndex) => { const elementId = Date.now() + index + 1000 + elIndex; // Handle import_guid.json format (type + content) if (el.type && el.content) { if (el.type === 'text') { return { id: elementId, type: 'text', title: 'Text Block', visible: true, position: elIndex, textContent: el.content, textWidth: 100 }; } else if (el.type === 'metadata') { return { id: elementId, type: 'metadata', title: 'Metadata', visible: true, position: elIndex, metaTitle: el.metaTitle || title, metaDescription: el.metaDescription || '', metaKeywords: el.metaKeywords || '' }; } } // Handle existing format (already has id, position, etc.) return { ...el, id: elementId, position: el.position !== undefined ? el.position : elIndex }; }); } } else { // Otherwise (for GUID-based import), generate the page structure from scratch. console.log(`Generating new pageElements for page: "${title}"`); pageElements.push({ id: newItemId + 1000, type: "metadata", title: "Metadata", visible: true, position: 0, metaTitle: classicItem.meta?.title || title, metaDescription: classicItem.meta?.description || "", metaKeywords: classicItem.meta?.keywords || "" }); // Add images if they exist in the page if (classicItem.images && classicItem.images.length > 0) { const slides = classicItem.images.map((img) => { const imageFilename = (img.filename || `image_${Date.now()}`) + (img.metadata?.extension || '.jpg'); const imageKey = `${itemGuid}/images/${imageFilename}`; const imageUrl = `https://storage.neonsky.app/${imageKey}`; return { imageUrl, imageKey, imageFilename, imageType: img.metadata?.mimetype || 'image/jpeg', caption: cleanDescriptionLeadingLines(fixMultilineLinks(convertPlainTextLinks(img.caption || ''))), title: img.title || '', byline: img.byline || '', dateline: img.dateline || '', altText: img.metadata?.alt_text || '' }; }); pageElements.push({ id: newItemId + 1500, type: "slideshow", title: "Page Images", visible: true, position: 1, slides, slideDuration: 5000, transitionDuration: 500, slideshowWidth: 100, slideshowHeight: 60, showFullImages: true }); } pageElements.push({ id: newItemId + 2000, type: "text", title: "Text Block Spacer", visible: true, position: 2, textContent: "

 


", textWidth: 100 }); const columns = []; const leftColumnElements = []; const rightColumnElements = []; if (classicItem.images && classicItem.images.length > 0) { const slides = classicItem.images.map((img) => { const imageFilename = (img.filename || `image_${Date.now()}`) + (img.metadata?.extension || '.jpg'); const imageKey = `${itemGuid}/images/${imageFilename}`; const imageUrl = `https://storage.neonsky.app/${imageKey}`; // Preserve all image metadata including captions, titles, etc. return { imageUrl, imageKey, imageFilename, imageType: img.metadata?.mimetype || 'image/jpeg', // Preserve caption information - handle both direct and language version storage patterns caption: cleanDescriptionLeadingLines(fixMultilineLinks(convertPlainTextLinks(img.caption || img.languageVersions?.[0]?.caption || ''))), title: img.title || img.languageVersions?.[0]?.title || '', byline: img.byline || img.languageVersions?.[0]?.byline || '', dateline: img.dateline || img.languageVersions?.[0]?.dateline || '', altText: img.metadata?.alt_text || '', // Preserve any additional metadata metaTitle: img.metadata?.meta_title || '', metaDescription: img.metadata?.meta_description || '', metaKeywords: img.metadata?.meta_keywords || '', // Preserve gallery-specific data sortOrder: img.galleryData?.sortOrder || 0, linkURL: img.galleryData?.linkURL || null, linkTarget: img.galleryData?.linkTarget || '_self' }; }); leftColumnElements.push({ id: newItemId + 3100, type: "slideshow", title: "Image Slideshow", visible: true, position: 0, slides, slideDuration: 5000, transitionDuration: 500, slideshowWidth: 88, slideshowHeight: 50, showFullImages: true }); } let rawDetails = classicItem.details || classicItem.languageVersions?.[0]?.details || ""; if (rawDetails && typeof rawDetails === 'string') { // ================================================================= // CHANGE 2: Fix for "Invalid regular expression flags" error. // Changed from /.../gi literal to new RegExp('...', 'gi') constructor // to prevent syntax errors. // ================================================================= let processedText = rawDetails.replace(new RegExp('', 'gi'), '').replace(new RegExp('', 'gi'), ''); processedText = processedText.replace(new RegExp('

', 'gi'), '

'); processedText = processedText.replace(new RegExp('', 'gi'), '').replace(new RegExp('', 'gi'), ''); processedText = processedText.replace(new RegExp('', 'gi'), '').replace(new RegExp('', 'gi'), ''); processedText = processedText.replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&'); // Apply description fixes to processed text processedText = cleanDescriptionLeadingLines(processedText); processedText = fixMultilineLinks(processedText); processedText = convertPlainTextLinks(processedText); rightColumnElements.push({ id: newItemId + 3200, type: "text", title: "Text Block Content", visible: true, position: 0, textContent: processedText, textWidth: 70 }); } columns.push({ id: newItemId + 3300, hAlign: "right", vAlign: "middle", elements: leftColumnElements }); columns.push({ id: newItemId + 3400, hAlign: "left", vAlign: "middle", elements: rightColumnElements }); pageElements.push({ id: newItemId + 3000, type: "column-container", title: "Imported Columns", visible: true, position: 2, columns, backgroundSlideshow: { enabled: false, slides: [], slideDuration: 5000, transitionDuration: 500, slideshowHeight: 50, showFullImages: false } }); } newItem = { id: newItemId, title, isPage: true, isIntegrated: true, isSubmenu: false, pageId: pageUniqueId, visible: classicItem.visible !== false, parentId: null, siteId: currentSiteId, slug: finalSlug, url: `/${finalSlug}`, pageElements, classicCategoryId: classicItem.categoryID, classicGuid: itemGuid, importSource: 'classic-page' }; } else if (classicItem.isFolder === true) { // --- Handle folders from import_guid.json format --- console.log(`Creating folder: "${title}"`); newItem = { id: newItemId, title, isFolder: true, isCollapsed: classicItem.isCollapsed !== false, visible: classicItem.visible !== false, parentId: null, siteId: currentSiteId, slug: finalSlug, importSource: 'folder' }; } else { // --- This is a Gallery --- const galleryPageId = generatePageId(); // --- ENHANCED: Try to fetch and include actual image data with captions --- let enhancedGalleryOptions = { manualCollectionName: `GUID=${itemGuid}`, categoryId: classicItem.categoryID, importedFromClassic: true, siteId: currentSiteId, pageId: galleryPageId }; // --- ENHANCED: Preserve gallery display settings from Classic JSON --- if (classicItem.galleryOptions) { console.log(`Preserving gallery settings for: ${title}`); // Preserve layout and display settings const preservedSettings = [ 'startInSingles', 'layoutType', 'columns', 'spacing', 'showDescription', 'showFilename', 'displayAllInfo', 'navigationMode', 'showTextBlock', 'fixedHeroImage', 'zoomInLightbox', 'lightboxOnMobile', 'fadeDuration', 'fadeInDuration', 'descriptionTextColor', 'gridImageOverlayColor', 'gridImageOverlayOpacity', 'lightboxBgColor', 'lightboxBgOpacity', 'lightboxCloseColor', 'lightboxArrowColor', 'useTitles', 'useLinks', 'desktopTitleDisplayMode', 'titleTextAlign', 'showDescriptionInOverlay', 'includeRolloverImageInOverlay', 'filterMenuEnabled', 'filterMenuCollectionNames', 'filterMenuTitles', 'filterMenuStyle', 'rolloverSwap', 'rolloverCollectionNames', 'openMultipleLightboxes', 'batchSize', 'autoplaySingles', 'autoplayDuration', 'autoplayTransition' ]; preservedSettings.forEach(setting => { if (classicItem.galleryOptions[setting] !== undefined) { enhancedGalleryOptions[setting] = classicItem.galleryOptions[setting]; } }); // Preserve text content if (classicItem.galleryOptions.horizontalScrollerText) { enhancedGalleryOptions.horizontalScrollerText = classicItem.galleryOptions.horizontalScrollerText; } // Preserve JSON collections if present if (classicItem.galleryOptions.jsonCollections) { enhancedGalleryOptions.jsonCollections = classicItem.galleryOptions.jsonCollections; } else { // Create jsonCollections structure with processed image metadata const collectionKey = 'GUID=' + itemGuid; const processedImages = classicItem.images || []; enhancedGalleryOptions.jsonCollections = { [collectionKey]: { isClassicCollection: true, guid: collectionKey, metadata: processedImages, totalItems: processedImages.length } }; console.log('Created jsonCollections for ' + title + ' with ' + processedImages.length + ' images'); // Build and try to upload NDJSON for this gallery. // If upload succeeds, switch to metadata = {} so frontend fetches NDJSON. // Skip NDJSON generation for galleries with no images to avoid creating empty files if (processedImages.length > 0) { try { console.log('Building and uploading NDJSON for gallery:', title, '(' + processedImages.length + ' images)'); const ndjsonContent = buildHydraClassicNdjson(itemGuid, processedImages); console.log('Generated NDJSON content length:', ndjsonContent.length, 'characters'); const uploadResult = await uploadNdjsonToTigris(currentSiteId, galleryPageId, ndjsonContent); if (uploadResult && uploadResult.success) { try { enhancedGalleryOptions.jsonCollections[collectionKey].metadata = {}; enhancedGalleryOptions.jsonCollections[collectionKey].totalItems = processedImages.length; console.log('NDJSON uploaded successfully for "' + title + '"; switched metadata to {}'); // Store dual upload metadata if Irys was successful if (uploadResult.irysResult && uploadResult.irysResult.txId) { enhancedGalleryOptions.txId = uploadResult.irysResult.txId; enhancedGalleryOptions.permaURL = uploadResult.irysResult.txId; // Plain txId for racing enhancedGalleryOptions.isPerma = true; enhancedGalleryOptions.uploadedTo = uploadResult.uploadedTo; enhancedGalleryOptions.fileSize = uploadResult.fileSize; enhancedGalleryOptions.siteId = currentSiteId; enhancedGalleryOptions.pageId = galleryPageId; console.log('✅ Dual upload complete - Irys txId:', uploadResult.irysResult.txId); console.log(' Uploaded to:', uploadResult.uploadedTo.join(' + ')); } else { console.log('✅ Single upload (Fly.io only) - No Irys data'); } } catch (applyErr) { console.warn('Failed to switch metadata to {} after NDJSON upload:', applyErr); } } else { console.warn('NDJSON upload failed for "' + title + '"; keeping inline metadata array'); } } catch (e) { console.warn('NDJSON build/upload skipped for "' + title + '" due to error:', e); } } else { console.log('Skipping NDJSON generation for "' + title + '" - no images to process'); } } // Preserve additional metadata if (classicItem.galleryOptions.siteAlias) { enhancedGalleryOptions.siteAlias = classicItem.galleryOptions.siteAlias; } if (classicItem.galleryOptions.initialPageUuid) { enhancedGalleryOptions.initialPageUuid = classicItem.galleryOptions.initialPageUuid; } if (classicItem.galleryOptions.initialPageAlias) { enhancedGalleryOptions.initialPageAlias = classicItem.galleryOptions.initialPageAlias; } if (classicItem.galleryOptions.timestamp) { enhancedGalleryOptions.timestamp = classicItem.galleryOptions.timestamp; } } // Note: For import from pasted JSON, we don't include image data to avoid overwriting existing captions // Image data is only included when importing from Classic GUID directly console.log(`Gallery ${title} imported with structure and settings preserved (image data not included to avoid overwriting existing captions)`); newItem = { id: newItemId, title, url: `/${finalSlug}`, isIntegrated: true, isPage: false, isSubmenu: false, visible: classicItem.visible !== false, parentId: null, siteId: currentSiteId, pageId: galleryPageId, classicCategoryId: classicItem.categoryID, classicGuid: itemGuid, importSource: 'classic-gallery', slug: finalSlug, normalizedName: finalSlug, galleryOptions: enhancedGalleryOptions }; } tempNewItems.push(newItem); } // --- Pass 2: Link children to parents using the ID map --- tempNewItems.forEach(newItem => { const originalId = Object.keys(originalIdToNewIdMap).find(key => originalIdToNewIdMap[key] === newItem.id); const originalItem = galleriesForImport.find(g => String(g.id) === String(originalId)); if (originalItem && originalItem.parentId) { const newParentId = originalIdToNewIdMap[originalItem.parentId]; if (newParentId) { newItem.parentId = newParentId; const parentItem = tempNewItems.find(p => p.id === newParentId); if (parentItem) { parentItem.isSubmenu = true; parentItem.isCollapsed = true; } } else { newItem.parentId = submenuParentId; } } else { newItem.parentId = submenuParentId; } }); if (replaceMenu) { console.log("Replacing existing menu structure."); // If replacing, overwrite the entire galleries array. galleries = tempNewItems; } else { console.log("Appending to existing menu structure."); // If not replacing, push the new items to the existing array. galleries.push(...tempNewItems); } if (progressBar) progressBar.style.width = '90%'; if (statusMessage) statusMessage.textContent = 'Saving changes...'; // Pass import flag to skip localStorage for large import operations await saveGalleries({ isImport: true }); if (progressBar) progressBar.style.width = '100%'; if (statusMessage) statusMessage.textContent = `Successfully imported ${tempNewItems.length} items!`; if (statusEl) statusEl.classList.remove('importing'); setTimeout(() => { renderGalleries(); toggleImportClassicForm(); if (progressBar) progressBar.style.width = '0'; if (statusEl) statusEl.style.display = 'none'; if (confirm('Import complete! Would you like to reload the page to ensure everything is displayed correctly?')) { window.location.reload(); } }, 1000); } catch (error) { console.error('Error importing classic galleries:', error); if (statusMessage) statusMessage.textContent = `Error: ${error.message}`; if (progressBar) { progressBar.style.width = '100%'; progressBar.style.backgroundColor = '#dc3545'; } if (statusEl) statusEl.classList.remove('importing'); } } /** * Improved toggleImportClassicForm - Closes other forms first */ function toggleImportClassicForm() { // First check if user is logged in and in edit mode if (!isAuthenticated || !isAdmin || !window.isInEditMode()) { alert('You must be logged in as an admin and in edit mode to import galleries.'); return; } const formId = 'importClassicForm'; const form = document.getElementById(formId); if (!form) { console.error('Import form element not found'); return; } // If this form is already open, just close everything if (window.currentOpenForm === formId) { closeAllForms(); return; } // Otherwise close all forms and open this one closeAllForms(); // Show the form form.style.display = 'block'; // Use setTimeout to allow the display change to take effect before adding the class setTimeout(() => { form.classList.add('visible'); }, 10); // Populate parent options const parentSelect = document.getElementById('importParent'); if (parentSelect) { parentSelect.innerHTML = ''; // Add all galleries that could be parents (including submenus) galleries.forEach(gallery => { // Only include items that are not already marked as imported if (!gallery.importSource) { parentSelect.innerHTML += ``; } }); } // Set default values const submenuTitle = document.getElementById('submenuTitle'); if (submenuTitle && !submenuTitle.value) { submenuTitle.value = 'Classic Galleries'; } // Reset any previous import status const statusEl = document.getElementById('importStatus'); if (statusEl) { statusEl.style.display = 'none'; const progressBar = statusEl.querySelector('.progress-bar'); if (progressBar) { progressBar.style.width = '0'; progressBar.style.backgroundColor = '#4682B4'; // Reset to blue } } // Set this as the current open form window.currentOpenForm = formId; } // Add event listener for the radio buttons to adjust height when page is selected document.addEventListener('DOMContentLoaded', function() { const galleryTypeRadios = document.querySelectorAll('input[name="galleryType"]'); galleryTypeRadios.forEach(radio => { radio.addEventListener('change', function() { const addForm = document.getElementById('addForm'); if (addForm && addForm.classList.contains('visible')) { const isPageSelected = this.value === 'page'; // Give more height for page type if (isPageSelected) { addForm.style.maxHeight = '600px'; } else { // For other types, measure content height const contentHeight = addForm.scrollHeight + 30; addForm.style.maxHeight = Math.max(contentHeight, 500) + 'px'; } } }); }); }); /** * Improved toggleStyleEditor - Closes other forms first */ function toggleStyleEditor(display = false) { const formId = 'menuStyleEditor'; // If this form is already open or display is false, just close everything if (window.currentOpenForm === formId || display === false) { closeAllForms(); return; } // Otherwise close all forms and open this one closeAllForms(); // Check if style editor already exists let styleEditor = document.getElementById(formId); if (!styleEditor) { // Get the edit controls element (to place the editor after it) const editControls = document.querySelector('.edit-controls'); if (editControls && window.MenuStyleCustomizer) { // Create a wrapper for the style editor styleEditor = document.createElement('div'); styleEditor.id = formId; styleEditor.style.display = 'none'; // Insert the editor after the edit controls editControls.parentNode.insertBefore(styleEditor, editControls.nextSibling); // Create the style editor UI window.MenuStyleCustomizer.createStyleEditor(styleEditor); } } // Show the editor if (styleEditor) { styleEditor.style.display = 'block'; // Set this as the current open form window.currentOpenForm = formId; } } /** * This function closes all forms when escape key is pressed */ document.addEventListener('keydown', function(e) { if (e.key === 'Escape' && window.currentOpenForm) { closeAllForms(); } }); /** * This function closes all forms when clicking outside any form */ document.addEventListener('click', function(e) { // Only process if a form is open if (!window.currentOpenForm) return; // Get the current open form element const currentForm = document.getElementById(window.currentOpenForm); if (!currentForm) return; // Check if the click was inside the form or on a form toggle button const isInsideForm = currentForm.contains(e.target); const isFormToggleButton = e.target.closest('[onclick*="toggle"]') !== null; // If clicked outside the form and not on a toggle button, close all forms if (!isInsideForm && !isFormToggleButton) { closeAllForms(); } }); // Override the original functions with our improved versions window.toggleAddForm = toggleAddForm; window.toggleStyleEditor = toggleStyleEditor; window.toggleMetadataEditor = toggleMetadataEditor; window.toggleSidebarElementForm = toggleSidebarElementForm; window.toggleImportClassicForm = toggleImportClassicForm; window.closeAllForms = closeAllForms; /** * Opens the edit form for a gallery item. * (UPDATED to include Meta Title and Meta Description fields, removed Parent Menu) * @param {number} id - The ID of the gallery item. * @param {Event} event - The click event. */ function editGallery(id, event) { event.stopPropagation(); // Prevent event bubbling const gallery = findGalleryById(galleries, id); const li = document.querySelector(`li[data-id="${id}"]`); if (gallery && li) { const showUrlField = gallery.isPage || gallery.isIntegrated; const showExternalUrlField = gallery.isExternal; const currentSlug = gallery.slug || (gallery.url && gallery.url.startsWith('/') ? gallery.url.substring(1) : ''); const currentExternalUrl = gallery.url || ''; // Clear existing content and rebuild form to ensure event listeners are fresh if needed li.innerHTML = ''; // Clear previous form if any const formDiv = document.createElement('div'); formDiv.className = 'edit-form'; formDiv.innerHTML = `
${showUrlField ? `
Path after domain name (e.g., yoursite.com/my-gallery). Use lowercase letters, numbers, and hyphens.
` : ''} ${showExternalUrlField ? `
Full URL for the external link (e.g., https://example.com/page).
` : ''}
Overrides site title for this item in search results/browser tabs.
Overrides site description for this item in search results.
Allows for a full-screen page or gallery layout. Menu remains visible in edit mode.
Visitors will need to enter a password to access this page.
Password visitors must enter to access this page.

`; li.appendChild(formDiv); // Append the new form // Add event listeners programmatically const saveButton = formDiv.querySelector(`#saveEditBtn-${id}`); if (saveButton) { saveButton.addEventListener('click', (e) => saveEdit(id, e)); } const cancelButton = formDiv.querySelector(`#cancelEditBtn-${id}`); if (cancelButton) { cancelButton.addEventListener('click', () => renderGalleries()); } // Add password protection toggle functionality const passwordProtectedCheckbox = formDiv.querySelector('.edit-password-protected'); const passwordField = formDiv.querySelector('.password-field'); if (passwordProtectedCheckbox && passwordField) { passwordProtectedCheckbox.addEventListener('change', function() { passwordField.style.display = this.checked ? 'block' : 'none'; }); } li.classList.add('edit-mode'); } else { console.error("Could not find gallery or list item for ID:", id); } } /** * Handles click on a folder item to show/hide its contents * @param {number} id - The gallery ID * @param {Event} event - The click event */ function toggleFolder(id, event) { // Ensure the event doesn't interfere with drag operations event.stopPropagation(); console.log('Toggling folder:', id); const gallery = galleries.find(g => g.id === id); if (!gallery) return; // Toggle the collapsed state gallery.isCollapsed = !gallery.isCollapsed; // Find the list item in the DOM const listItem = document.querySelector(`li[data-id="${id}"]`); if (listItem) { // Toggle the expanded class listItem.classList.toggle('expanded', !gallery.isCollapsed); // Find the submenu toggle icon and rotate it const toggleIcon = listItem.querySelector('.toggle-icon'); if (toggleIcon) { toggleIcon.classList.toggle('rotated', !gallery.isCollapsed); } const folderSpan = listItem.querySelector('.menu-item[data-folder="true"]'); } // Only save if in edit mode if (window.isInEditMode && window.isInEditMode()) { saveGalleries(); } else { // For non-edit mode, optionally save folder states to localStorage for persistence try { const folderStates = JSON.parse(localStorage.getItem('folder_states') || '{}'); folderStates[id] = !gallery.isCollapsed; localStorage.setItem('folder_states', JSON.stringify(folderStates)); } catch (e) { console.error('Error saving folder state to localStorage:', e); } } } window.toggleFolder = toggleFolder; /** * Toggle password protection for a gallery/page * @param {number} id - The gallery ID * @param {Event} event - The click event */ function toggleGalleryPasswordProtection(id, event) { event.stopPropagation(); const gallery = findGalleryById(galleries, id); if (!gallery) { console.error("Could not find gallery for password protection toggle, ID:", id); return; } if (gallery.passwordProtected) { // Remove password protection if (confirm('Remove password protection from this page?')) { gallery.passwordProtected = false; delete gallery.password; saveGalleries(); renderGalleries(); } } else { // Add password protection const password = prompt('Enter a password for this page:'); if (password && password.trim()) { gallery.passwordProtected = true; gallery.password = password.trim(); saveGalleries(); renderGalleries(); } } } window.toggleGalleryPasswordProtection = toggleGalleryPasswordProtection; /** * Saves the edited gallery item details. * (UPDATED to handle Meta Title and Meta Description, removed Parent Menu logic) * @param {number} id - The ID of the gallery item. * @param {Event} event - The click event. */ function saveEdit(id, event) { event.stopPropagation(); const li = document.querySelector(`li[data-id="${id}"]`); if (!li) { console.error("Could not find list item for saveEdit, ID:", id); return; } const titleInput = li.querySelector('.edit-title'); const slugInput = li.querySelector('.edit-slug'); const externalUrlInput = li.querySelector('.edit-external-url'); const metaTitleInput = li.querySelector('.edit-meta-title'); const metaDescriptionInput = li.querySelector('.edit-meta-description'); const hideMenuInput = li.querySelector('.edit-hide-menu'); const passwordProtectedInput = li.querySelector('.edit-password-protected'); const passwordInput = li.querySelector('.edit-password'); const title = titleInput ? titleInput.value.trim() : ''; const newSlugRaw = slugInput ? slugInput.value.trim() : null; const externalUrl = externalUrlInput ? externalUrlInput.value.trim() : ''; const metaTitle = metaTitleInput ? metaTitleInput.value.trim() : ''; const metaDescription = metaDescriptionInput ? metaDescriptionInput.value.trim() : ''; const hideMenu = hideMenuInput ? hideMenuInput.checked : false; const passwordProtected = passwordProtectedInput ? passwordProtectedInput.checked : false; const password = passwordInput ? passwordInput.value.trim() : ''; const gallery = findGalleryById(galleries, id); if (!gallery) { console.error("Could not find gallery object for saveEdit, ID:", id); renderGalleries(); return; } if (!title && !gallery.isSpacer) { alert('Please enter a title'); return; } // Validate password protection if (passwordProtected && !password) { alert('Please enter a password when password protection is enabled'); return; } gallery.title = title; gallery.metaTitle = metaTitle; gallery.metaDescription = metaDescription; gallery.hideMenuOnPage = hideMenu; // Save the new property // Save password protection settings gallery.passwordProtected = passwordProtected; if (passwordProtected) { gallery.password = password; } else { delete gallery.password; // Remove password if protection is disabled } if ((gallery.isPage || gallery.isIntegrated) && newSlugRaw !== null) { const originalSlug = gallery.slug || (gallery.url && gallery.url.startsWith('/') ? gallery.url.substring(1) : ''); let sanitizedSlug = slugify(newSlugRaw); if (!sanitizedSlug) { sanitizedSlug = slugify(gallery.title); } if (sanitizedSlug !== originalSlug) { const otherGalleries = galleries.filter(g => g.id !== id); gallery.slug = ensureUniqueSlug(sanitizedSlug, otherGalleries); gallery.url = `/${gallery.slug}`; } else { if (gallery.url !== `/${gallery.slug}`) { gallery.url = `/${gallery.slug}`; } } } // Update external URL if this is an external link if (gallery.isExternal && externalUrl) { gallery.url = externalUrl; } saveGalleries(); renderGalleries(); // If the currently active gallery was just edited, re-apply menu visibility if (gallery.id === activeGalleryId) { const bodyEl = document.body; if (gallery.hideMenuOnPage && !isInEditMode()) { bodyEl.classList.add('menu-hidden-on-page'); } else { bodyEl.classList.remove('menu-hidden-on-page'); } } } // Helper function to check if a gallery is an ancestor of another function hasAncestor(galleryId, ancestorId) { if (galleryId === ancestorId) return true; const gallery = galleries.find(g => g.id === galleryId); if (!gallery || !gallery.parentId) return false; if (gallery.parentId === ancestorId) return true; return hasAncestor(gallery.parentId, ancestorId); } function deleteGallery(id, event) { event.stopPropagation(); if (confirm('Are you sure you want to delete this gallery?')) { galleries = galleries.filter(g => g.id !== id); if (activeGalleryId === id) { activeGalleryId = null; document.getElementById('galleryFrame').src = ''; } saveGalleries(); renderGalleries(); } } function debugNestedStructure() { console.group('Current Gallery Structure'); function printGallery(gallery, level = 0) { const indent = ' '.repeat(level); console.log(`${indent}${gallery.title} (ID: ${gallery.id}, Parent: ${gallery.parentId || 'none'})`); if (gallery.children && gallery.children.length > 0) { gallery.children.forEach(child => printGallery(child, level + 1)); } } // Create a tree structure for debugging const galleryTree = createGalleryTree(galleries); galleryTree.forEach(gallery => printGallery(gallery)); console.groupEnd(); } function toggleHome(id, event) { event.stopPropagation(); console.log('Setting home page to gallery:', id); // Find gallery const gallery = galleries.find(g => g.id === id); if (!gallery) return; // Check if this gallery is already set as home const alreadyHome = gallery.isHomePage === true; // Set all galleries to not be the home page galleries.forEach(g => { g.isHomePage = false; }); // Toggle this gallery's home status - if it was already home, we're unsetting it gallery.isHomePage = !alreadyHome; // Save changes saveGalleries(); // Update UI renderGalleries(); // If this gallery is now the home page, display a message if (gallery.isHomePage) { const title = gallery.title || 'This page'; showSuccessMessage(`${title} is now set as the Home page`); } } // Function to toggle gallery visibility function toggleVisibility(id, event) { event.stopPropagation(); const gallery = galleries.find(g => g.id === id); if (gallery) { // Toggle the visibility gallery.visible = gallery.visible === false ? true : false; // Save the change to server saveGalleries(); // Update UI - important to toggle the class on the button itself const toggle = event.currentTarget; if (toggle) { if (gallery.visible === false) { toggle.classList.add('hidden'); // Update the SVG icon toggle.querySelector('svg').innerHTML = ''; } else { toggle.classList.remove('hidden'); // Update the SVG icon toggle.querySelector('svg').innerHTML = ''; } } // Update parent row class const listItem = document.querySelector(`li[data-id="${id}"]`); if (listItem) { if (gallery.visible === false) { listItem.classList.add('hidden-gallery'); } else { listItem.classList.remove('hidden-gallery'); } } // Only clear iframe if not in edit mode if (id === activeGalleryId && gallery.visible === false && !isEditing) { document.getElementById('galleryFrame').src = ''; } // Re-render menus based on current layout const currentLayout = document.body.classList.contains('menu-layout-horizontal') ? 'horizontal' : document.body.classList.contains('menu-layout-top') ? 'top' : 'sidebar'; if (currentLayout === 'horizontal' && typeof window.renderHorizontalMenu === 'function') { window.renderHorizontalMenu(); } // Also re-render the sidebar gallery tree to ensure hidden-gallery class is properly applied // This is especially important when horizontal layout is active but we're viewing the sidebar in edit mode if (typeof window.renderGalleries === 'function') { window.renderGalleries(); } else if (typeof renderGalleries === 'function') { renderGalleries(); } } } /** * Simplified loadGallery function that follows the same content swap pattern * @param {number} id - The gallery ID * @param {Event} event - Optional event object */ // Enhanced loadGallery function with script cleanup async function loadGallery(id, event) { // Stop event propagation if provided if (event) { event.stopPropagation(); } const galleryContainer = document.querySelector('.gallery-container'); if (galleryContainer) { // Apply immediate hiding styles galleryContainer.style.opacity = '0'; } console.log('Loading gallery with ID:', id); // Find gallery by ID const gallery = findGalleryById(galleries, id); // Assuming `galleries` is accessible if (!gallery) { console.warn('No gallery found with ID:', id); if (galleryContainer) { galleryContainer.style.opacity = '1'; } return; } // Check if this is a page, if so use loadPage instead if (gallery.isPage === true) { // Use the loadPage function defined in this scope (or window.loadPage if you prefer) return loadPage(id, event); } // Skip submenu items if (gallery.isSubmenu) { console.log('Gallery is submenu, skipping URL update'); toggleSubmenu(id, event || { stopPropagation: () => {} }); // Assuming toggleSubmenu is available if (galleryContainer) { galleryContainer.style.opacity = '1'; } // Show container return; } console.log('Found gallery:', gallery.title); // CRITICAL: Set active gallery ID consistently in both global and window scope console.log('Setting active gallery ID to:', id, '(Previous value:', (window.activeGalleryId || activeGalleryId), ')'); window.activeGalleryId = id; activeGalleryId = id; // Ensure both variables are synchronized // Save reference to which gallery we're loading (for later verification) window.currentLoadingGalleryId = id; // CRITICAL: Thorough cleanup with await to ensure completion if (typeof window.removeGalleryScriptsWithPause === 'function') { await window.removeGalleryScriptsWithPause(); } // Clear all existing content from gallery container if (!galleryContainer) { console.error('Gallery container not found'); return; } // Force clear all content galleryContainer.innerHTML = ''; // CRITICAL: Double-check active gallery ID before updating UI if (window.activeGalleryId !== id || activeGalleryId !== id) { console.warn('Active gallery ID changed unexpectedly before UI update, restoring to:', id); window.activeGalleryId = id; activeGalleryId = id; } // Apply menu visibility based on the gallery's setting and edit mode const bodyEl = document.body; if (gallery.hideMenuOnPage && !isInEditMode()) { // Check if not in edit mode bodyEl.classList.add('menu-hidden-on-page'); } else { bodyEl.classList.remove('menu-hidden-on-page'); // Ensure menu is visible if in edit mode or not hidden } // Update UI state using explicit window function references or local fallbacks if (typeof window.updateActiveStates === 'function') { window.updateActiveStates(); } else if (typeof updateActiveStates === 'function') { updateActiveStates(); } if (typeof window.updateMobileTitle === 'function') { window.updateMobileTitle(); } else if (typeof updateMobileTitle === 'function') { updateMobileTitle(); } if (typeof window.closeMobileMenu === 'function') { window.closeMobileMenu(); } else if (typeof closeMobileMenu === 'function') { closeMobileMenu(); } // Update URL with gallery slug if (typeof window.updateURLWithGallerySlug === 'function') { window.updateURLWithGallerySlug(gallery); } else if (typeof updateURLWithGallerySlug === 'function') { updateURLWithGallerySlug(gallery); } // Create gallery container and load content const neonGalleryContainer = document.createElement('div'); neonGalleryContainer.id = 'neon-gallery-container'; neonGalleryContainer.className = 'gallery-direct-content'; neonGalleryContainer.setAttribute('data-gallery-id', gallery.id.toString()); neonGalleryContainer.style.width = '100%'; neonGalleryContainer.style.height = '100%'; galleryContainer.appendChild(neonGalleryContainer); // Add a cache-busting parameter to ensure script is freshly loaded const timestamp = Date.now(); // Set up gallery parameters window.Parameters = { SiteAlias: window.location.hostname, InitialPageUuid: gallery.pageId, InitialPageAlias: gallery.slug || slugify(gallery.title), // Assuming slugify is available isInEditor: window.isEditing || false, // Assuming window.isEditing is available siteId: window.siteId || gallery.siteId || '', // Assuming window.siteId is available isHydra: true, galleryInstanceId: timestamp, // Added loadedGalleryId as per your original function loadedGalleryId: gallery.id }; // Debug: Check what galleryOptions contains console.error('🔍 INDEX.TS DEBUG - Gallery:', gallery.title, 'has isPerma:', gallery.galleryOptions?.isPerma, 'permaURL:', gallery.galleryOptions?.permaURL, 'loadTxId:', gallery.galleryOptions?.loadTxId, 'loadPermaURL:', gallery.galleryOptions?.loadPermaURL); // Set up gallery config window.neonGalleryConfig = { useData: true, useCDN: true, version: 'live', manualCollectionName: gallery.galleryOptions?.manualCollectionName || "mod", layoutType: gallery.galleryOptions?.layoutType || "grid", // Spread gallery options AFTER setting defaults to ensure current gallery's settings take precedence // This ensures that only properties from the current gallery are included, not from previous galleries ...(gallery.galleryOptions || {}), // CRITICAL: Explicitly include Load Network fields to ensure they're passed to gallery script // These fields are required for the gallery to determine if it should use Load Network racing loadTxId: gallery.galleryOptions?.loadTxId || null, loadPermaURL: gallery.galleryOptions?.loadPermaURL || null, siteId: window.siteId || gallery.siteId || '', // Assuming window.siteId is available // Added galleryId and galleryInstanceId as per your original function - these OVERRIDE spread galleryId: gallery.id, galleryInstanceId: timestamp }; console.error('🔍 INDEX.TS DEBUG - After spread, window.neonGalleryConfig.isPerma:', window.neonGalleryConfig.isPerma, 'permaURL:', window.neonGalleryConfig.permaURL, 'loadTxId:', window.neonGalleryConfig.loadTxId, 'loadPermaURL:', window.neonGalleryConfig.loadPermaURL); // Create and add the gallery script with cache-busting // Use Worker route (avoids CDN SSL issues) const script = document.createElement('script'); const galleryScriptsBase = window.location.origin + '/gallery-scripts/'; script.src = galleryScriptsBase + 'neon-gallery-main-v260118-001.js'; // Using versioned file script.setAttribute('data-gallery-id', gallery.id.toString()); // Keep data-gallery-id attribute script.setAttribute('data-timestamp', timestamp.toString()); // Keep data-timestamp attribute console.log('Loading gallery-main script from Worker:', script.src); script.onerror = function() { console.error('❌ Gallery script failed, retrying via proxy:', script.src); const proxyUrl = script.src.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/'); script.src = proxyUrl; script.onerror = function() { console.error('❌ Proxy failed, trying storage:', proxyUrl); script.src = script.src.replace('cdn.neonsky.app', 'storage.neonsky.app'); }; }; // Added script.onload from your original function script.onload = function() { if (window.currentLoadingGalleryId !== gallery.id) { console.warn('Gallery ID mismatch! Expected:', gallery.id, 'Current:', window.currentLoadingGalleryId); } console.log(`Gallery script loaded for: ${gallery.title} (ID: ${gallery.id})`); }; if (galleryContainer) { setTimeout(() => { galleryContainer.style.opacity = '1'; }, 50); // Small delay to ensure content is ready } console.log(`Loading fresh gallery script for: ${gallery.title}`); document.body.appendChild(script); } function removeGalleryScriptsWithPause() { return new Promise(resolve => { console.log('Performing comprehensive gallery cleanup (v2 - refined selectors)'); // 1. Capture and clear any gallery data in localStorage (remains the same) try { const localStorageKeys = Object.keys(localStorage); const galleryStorageKeys = localStorageKeys.filter(key => key.includes('gallery') || key.includes('neon') || key.includes('lightbox') ); galleryStorageKeys.forEach(key => { localStorage.removeItem(key); }); } catch (e) { console.warn('Error clearing localStorage:', e); } // @ts-ignore window._galleryPageContext = null; // (remains the same) // 3. Remove all gallery-specific CSS styles (remains the same) const galleryStyles = document.querySelectorAll('style[data-gallery], link[href*="neon-gallery"]'); galleryStyles.forEach(style => { if (style && style.parentNode) { // console.log('Removing gallery style element'); // Console log can be verbose, optionally keep style.parentNode.removeChild(style); } }); // 4. Remove lightbox elements (more specific selectors now) // These are specific IDs and classes for lightbox components. const lightboxComponentSelectors = [ '#neon-lightbox', // Main lightbox container by ID '.neon-lightbox', // Alternative class for main container '.lightbox-container',// General container class if used // Add any other *specific* classes or IDs your lightbox system uses for its top-level elements ]; const lightboxElements = document.querySelectorAll(lightboxComponentSelectors.join(', ')); lightboxElements.forEach(lightboxEl => { if (lightboxEl && lightboxEl.parentNode && lightboxEl !== document.body && lightboxEl !== document.documentElement) { // console.log('Removing lightbox component:', lightboxEl.id || lightboxEl.className); lightboxEl.parentNode.removeChild(lightboxEl); } }); // 5. Remove other gallery-specific DOM elements // REMOVED: '[class*="lightbox"]', '[id*="lightbox"]' from this list // The selector '[class*="neon-"]' is still broad but less likely to match body unless you add "neon-" class to body. const galleryElementSelectors = [ '.neon-gallery-wrapper', '.gallery-container .fullscreen-overlay', '.neon-gallery-modal', '.gallery-tooltip', '.neon-gallery-context-menu', '.neon-thumbnails', '.neon-image', '.neon-caption', '.neon-controls', '.neon-pagination', '[class*="neon-"]', // BE CAUTIOUS: If this matches body or critical layout elements, it can cause issues. '[data-gallery-id]', '[data-image-id]', '.image-grid', '.image-masonry' // Ensure none of these selectors inadvertently match document.body or critical layout containers ]; const galleryElements = document.querySelectorAll(galleryElementSelectors.join(', ')); if (galleryElements.length > 0) { galleryElements.forEach(el => { // CRITICAL FIX: Add checks to ensure we don't remove body or html if (el && el.parentNode && el !== document.body && el !== document.documentElement) { // console.log('Removing gallery-specific DOM element:', el.id || el.className); el.parentNode.removeChild(el); } else if (el === document.body || el === document.documentElement) { console.warn(`[CRITICAL WARNING] Attempt to remove document.body or document.documentElement blocked by selector: ${el.id || el.className}. Review your selectors.`); } }); } // 6. Find and remove ALL gallery scripts (remains the same) const scripts = document.querySelectorAll( 'script[src*="neon-gallery-main-hydra"], script[src*="neon-gallery-main-dev.js"], ' + 'script[src*="gallery"], script[data-gallery-id], script[data-gallery], script[data-timestamp]' ); scripts.forEach(script => { if (script && script.parentNode) { // console.log('Removing script:', script.src || script.getAttribute('data-gallery-id')); script.parentNode.removeChild(script); } }); // 6b. Specifically remove gallery options scripts (but NOT CSS - CSS is needed across galleries) const optionsScripts = document.querySelectorAll( 'script[src*="galleryOptions"], script[data-options-js]' ); optionsScripts.forEach(script => { if (script && script.parentNode) { console.log('Removing gallery options script:', script.src || script.getAttribute('data-options-js')); script.parentNode.removeChild(script); } }); // NOTE: We intentionally do NOT remove gallery options CSS here // The CSS is needed across galleries and doesn't cause conflicts like scripts do setTimeout(() => { const remainingScripts = document.querySelectorAll( 'script[src*="neon-gallery-main-hydra"], script[src*="neon-gallery-main-dev.js"], ' + 'script[src*="gallery"], script[data-gallery-id], script[data-gallery], script[data-timestamp]' ); if (remainingScripts.length > 0) { // console.warn(`[DEBUG] ${remainingScripts.length} gallery scripts still found after removal attempt.`); } }, 50); // 7. Reset ALL known global state variables used by the gallery (remains the same) const resetGlobals = [ 'neonGalleryInitComplete', 'neonGalleryInitInProgress', 'neonGalleryLoaded', 'neonGalleryConfig', 'neonGalleryState', 'neonGalleryCache', 'neonGalleryImages', 'neonGallerySettings', 'neonLightbox', 'neonGalleryEventListeners', 'neonGalleryInstance', 'currentGalleryId', 'galleryData', 'imageCache', 'thumbnailCache', ]; resetGlobals.forEach(prop => { // @ts-ignore if (window[prop] !== undefined) { // @ts-ignore window[prop] = null; } }); // 7b. Reset gallery options panel state // Reset optionsVisible flag if it exists on window (for cross-module access) // @ts-ignore if (window.optionsVisible !== undefined) { // @ts-ignore window.optionsVisible = false; } // Reset options-related globals const optionsGlobals = [ 'generateOptionsUI', 'openOptionsOverlay', 'showOptionsPanel', 'hideOptionsPanel', 'toggleOptionsOverlay', 'optionsVisible' ]; optionsGlobals.forEach(prop => { // @ts-ignore if (window[prop] !== undefined && typeof window[prop] !== 'function') { // Only reset non-function properties (like flags) // Functions should remain available // @ts-ignore if (prop === 'optionsVisible') { // @ts-ignore window[prop] = false; } } }); // Short pause before resolving setTimeout(resolve, 150); }); } // Fixed clearAddForm function with proper variable names function clearAddForm() { // Clear the title input const titleInput = document.getElementById('galleryTitle'); if (titleInput) { titleInput.value = ''; } // Clear the URL input const urlInput = document.getElementById('galleryUrl'); if (urlInput) { urlInput.value = ''; } // Reset the parent selection const parentSelect = document.getElementById('galleryParent'); if (parentSelect) { parentSelect.value = ''; } // Reset radio buttons to external const externalRadio = document.querySelector('input[name="galleryType"][value="external"]'); if (externalRadio) { externalRadio.checked = true; } // Show URL input container - using a different variable name! const urlInputContainer = document.getElementById('urlInputContainer'); if (urlInputContainer) { urlInputContainer.style.display = 'block'; } else { console.log('URL input container not found'); } } /** * Ensures window.galleries is synchronized with the global galleries variable * Call this function after any modification to gallery settings * * @returns {boolean} True if synchronization was successful */ /** * Enhanced synchronizeGalleries function that checks ALL galleries * not just imported ones, and ensures settings are preserved * @returns {boolean} Success indicator */ function synchronizeGalleries() { console.log('Starting enhanced gallery synchronization...'); if (typeof galleries !== 'undefined' && Array.isArray(galleries)) { // First check if window.galleries exists and is different from galleries const needsSync = !window.galleries || !Array.isArray(window.galleries) || window.galleries.length !== galleries.length; // ENHANCED: Check ALL galleries for options preservation, not just imported ones if (!needsSync && window.galleries.length > 0) { // Find all galleries with galleryOptions const galleriesWithOptions = galleries.filter(g => g.galleryOptions && Object.keys(g.galleryOptions).length > 4 ); if (galleriesWithOptions.length > 0) { // Check ALL galleries with substantial options, not just a sample let syncNeeded = false; // Use for loop so we can break early if we find a mismatch for (let i = 0; i < galleriesWithOptions.length; i++) { const gallery = galleriesWithOptions[i]; const windowGallery = window.galleries.find(g => g.id === gallery.id); if (windowGallery) { // Get options depth const galleryOptionsDepth = gallery.galleryOptions ? Object.keys(gallery.galleryOptions).length : 0; const windowOptionsDepth = windowGallery.galleryOptions ? Object.keys(windowGallery.galleryOptions).length : 0; // If options depth differs significantly, we need to sync if (Math.abs(galleryOptionsDepth - windowOptionsDepth) > 2) { console.log(`Gallery sync needed: Options mismatch detected for ${gallery.title}`); console.log(`Options count: global=${galleryOptionsDepth}, window=${windowOptionsDepth}`); syncNeeded = true; break; // No need to check more galleries } // Additional check: Look for specific important gallery settings that should be preserved // Only do this check for galleries with substantial settings if (galleryOptionsDepth > 5 && windowOptionsDepth > 5) { // Critical keys that indicate gallery customization const criticalKeys = ['columns', 'spacing', 'layoutType', 'startInSingles', 'autoplaySingles', 'lightboxBgColor']; // Check if any critical keys are missing in window.galleries const missingKeys = criticalKeys.filter(key => gallery.galleryOptions[key] !== undefined && windowGallery.galleryOptions[key] === undefined ); if (missingKeys.length > 0) { console.log(`Gallery sync needed: Critical settings missing in window.galleries for ${gallery.title}`); console.log(`Missing keys: ${missingKeys.join(', ')}`); syncNeeded = true; break; // No need to check more galleries } } } } // If we detected a need to sync, do it now if (syncNeeded) { // Ensure we preserve rich gallery settings window.galleries = galleries.map(gallery => { const windowGallery = window.galleries.find(g => g.id === gallery.id); // If both have galleryOptions, merge them to ensure all settings are preserved if (windowGallery && windowGallery.galleryOptions && gallery.galleryOptions) { const globalOptionsDepth = Object.keys(gallery.galleryOptions).length; const windowOptionsDepth = Object.keys(windowGallery.galleryOptions).length; // If window gallery has more settings, merge them with global settings if (windowOptionsDepth > globalOptionsDepth) { return { ...gallery, galleryOptions: { ...windowGallery.galleryOptions, // Start with window settings ...gallery.galleryOptions // Override with any new global settings } }; } } // Otherwise use global gallery as is return gallery; }); console.log(`Synchronized window.galleries with global galleries (${galleries.length} items)`); return true; } } } // If we determined sync is needed (or as a precaution), update window.galleries if (needsSync || !window.galleries) { window.galleries = [...galleries]; // Create a shallow copy to ensure different reference console.log(`Synchronized window.galleries with global galleries (${galleries.length} items)`); } else { console.log('Gallery synchronization check: No sync needed'); } // Verify that all galleries with rich settings have their full options preserved const richGalleries = galleries.filter(g => g.galleryOptions && Object.keys(g.galleryOptions).length > 5 ); console.log(`Status: Found ${richGalleries.length} galleries with rich settings`); // Final verification - make sure both references are identical if (window.galleries !== galleries) { console.warn('References not identical after sync - forcing reference equality'); window.galleries = galleries; // Force reference equality return true; } return true; } else { console.warn('Cannot synchronize galleries: global galleries variable is undefined or not an array'); // If we have window.galleries but no global galleries, try to reverse-sync if (window.galleries && Array.isArray(window.galleries) && window.galleries.length > 0) { console.log('Attempting reverse sync: updating global galleries from window.galleries'); try { // This is risky and should only happen in unusual circumstances galleries = window.galleries; return true; } catch (e) { console.error('Could not reverse-sync galleries:', e); } } return false; } } /** * Loads NDJSON content from the original gallery * @param {string} siteId - The site ID * @param {string} pageId - The page ID of the original gallery * @returns {Promise} - The NDJSON content or null if not found/error */ async function loadGalleryNDJSON(siteId, pageId) { console.error('[NDJSON Duplication] loadGalleryNDJSON called with siteId:', siteId, 'pageId:', pageId); if (!siteId || !pageId) { console.error('[NDJSON Duplication] ERROR: Cannot load NDJSON - missing siteId or pageId', { siteId: siteId, pageId: pageId }); return null; } try { const ndjsonFilename = `${siteId}_${pageId}.ndjson`; const url = `https://fly.storage.tigris.dev/ns-bridge-pub/${ndjsonFilename}`; console.error('[NDJSON Duplication] Attempting to load NDJSON from:', url); console.error('[NDJSON Duplication] Filename:', ndjsonFilename); const response = await fetch(url); console.error('[NDJSON Duplication] Fetch response status:', response.status, response.statusText); if (!response.ok) { if (response.status === 404) { console.error('[NDJSON Duplication] ERROR: NDJSON file not found (404) - gallery may not have been published yet'); console.error('[NDJSON Duplication] Attempted URL:', url); return null; } const errorText = await response.text().catch(() => 'Could not read error text'); console.error('[NDJSON Duplication] ERROR: Failed to load NDJSON:', response.status, response.statusText, errorText); throw new Error(`Failed to load NDJSON: ${response.statusText}`); } const ndjsonContent = await response.text(); console.error('[NDJSON Duplication] SUCCESS: Loaded NDJSON, length:', ndjsonContent.length, 'characters'); console.error('[NDJSON Duplication] First 200 chars:', ndjsonContent.substring(0, 200)); return ndjsonContent; } catch (error) { console.error('[NDJSON Duplication] ERROR: Exception loading NDJSON:', error); console.error('[NDJSON Duplication] Error message:', error.message); console.error('[NDJSON Duplication] Error stack:', error.stack); return null; } } /** * Duplicates and publishes NDJSON for a gallery * @param {Object} originalGallery - The original gallery object * @param {Object} newGallery - The new gallery object with new pageId * @returns {Promise} - Success indicator */ async function duplicateGalleryNDJSON(originalGallery, newGallery) { console.error('[NDJSON Duplication] ===== STARTING duplicateGalleryNDJSON ====='); console.error('[NDJSON Duplication] Original gallery:', { id: originalGallery.id, title: originalGallery.title, pageId: originalGallery.pageId, classicGuid: originalGallery.classicGuid, hasGalleryOptions: !!originalGallery.galleryOptions, manualCollectionName: originalGallery.galleryOptions?.manualCollectionName, isClassicCollection: originalGallery.galleryOptions?.isClassicCollection }); console.error('[NDJSON Duplication] New gallery:', { id: newGallery.id, title: newGallery.title, pageId: newGallery.pageId }); try { // Check if this is an NDJSON-based gallery (not classic collection) // IMPORTANT: A gallery can have a GUID-style manualCollectionName but still use NDJSON files // Only skip if it's explicitly marked as a classic collection OR has no pageId (which means no NDJSON) // Having a pageId means it uses NDJSON files, so we should duplicate them const isClassicCollection = originalGallery.galleryOptions?.isClassicCollection === true; console.error('[NDJSON Duplication] Classic collection check:', { hasClassicGuid: !!originalGallery.classicGuid, manualCollectionName: originalGallery.galleryOptions?.manualCollectionName, startsWithGUID: originalGallery.galleryOptions?.manualCollectionName?.startsWith('GUID='), isClassicCollectionFlag: originalGallery.galleryOptions?.isClassicCollection, hasPageId: !!originalGallery.pageId, isClassicCollection: isClassicCollection, willSkip: isClassicCollection && !originalGallery.pageId }); // Only skip if explicitly marked as classic collection AND has no pageId // If it has a pageId, it uses NDJSON files and we should duplicate them if (isClassicCollection && !originalGallery.pageId) { console.error('[NDJSON Duplication] SKIP: Gallery is explicitly marked as classic collection with no pageId, skipping NDJSON duplication'); return true; // Not an error, just not applicable } // Check if original gallery has a pageId if (!originalGallery.pageId) { console.error('[NDJSON Duplication] SKIP: Original gallery has no pageId, skipping NDJSON duplication'); console.error('[NDJSON Duplication] Original gallery keys:', Object.keys(originalGallery)); return true; // Not an error, just not applicable } // Get siteId const siteId = window.siteId || originalGallery.siteId; console.error('[NDJSON Duplication] SiteId resolution:', { windowSiteId: window.siteId, gallerySiteId: originalGallery.siteId, resolvedSiteId: siteId }); if (!siteId) { console.error('[NDJSON Duplication] ERROR: Cannot duplicate NDJSON - missing siteId'); console.error('[NDJSON Duplication] window.siteId:', window.siteId); console.error('[NDJSON Duplication] originalGallery.siteId:', originalGallery.siteId); return false; } // Load original NDJSON console.error('[NDJSON Duplication] Loading original NDJSON for duplication...'); console.error('[NDJSON Duplication] Using siteId:', siteId, 'pageId:', originalGallery.pageId); const originalNdjson = await loadGalleryNDJSON(siteId, originalGallery.pageId); if (!originalNdjson) { console.error('[NDJSON Duplication] ERROR: Could not load original NDJSON, gallery may not have been published yet'); console.error('[NDJSON Duplication] This means the original gallery\'s NDJSON file was not found'); return false; } // Publish the duplicated NDJSON with new pageId using the same endpoint as edition publisher console.error('[NDJSON Duplication] Publishing duplicated NDJSON with new pageId:', newGallery.pageId); const dataFileName = `${siteId}_${newGallery.pageId}.ndjson`; console.error('[NDJSON Duplication] New filename:', dataFileName); console.error('[NDJSON Duplication] NDJSON content length:', originalNdjson.length); try { // Call the publish endpoint (same as edition publisher) console.error('[NDJSON Duplication] Calling publish endpoint: https://hydra-press-v2.fly.dev/publish'); const response = await fetch('https://hydra-press-v2.fly.dev/publish', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ ndjsonContent: originalNdjson, dataFileName: dataFileName }), }); console.error('[NDJSON Duplication] Publish response status:', response.status, response.statusText); console.error('[NDJSON Duplication] Response headers:', Object.fromEntries(response.headers.entries())); if (!response.ok) { const errorText = await response.text().catch(() => 'Unknown error'); console.error('[NDJSON Duplication] ERROR: Failed to publish duplicated NDJSON:', response.status, errorText); console.error('[NDJSON Duplication] Response status:', response.status); console.error('[NDJSON Duplication] Response statusText:', response.statusText); return false; } const result = await response.json(); console.error('[NDJSON Duplication] SUCCESS: Published duplicated NDJSON'); console.error('[NDJSON Duplication] Publish result:', JSON.stringify(result, null, 2)); console.error('[NDJSON Duplication] Publish result URLs:', result.urls); // Note: The publish endpoint doesn't return Irys txId, so we don't update galleryOptions // with txId/permaURL here. If Irys upload is needed, it would need to be done separately. console.error('[NDJSON Duplication] ===== duplicateGalleryNDJSON COMPLETED SUCCESSFULLY ====='); return true; } catch (error) { console.error('[NDJSON Duplication] ERROR: Exception publishing duplicated NDJSON:', error); console.error('[NDJSON Duplication] Error message:', error.message); console.error('[NDJSON Duplication] Error stack:', error.stack); return false; } } catch (error) { console.error('[NDJSON Duplication] ERROR: Unexpected error in duplicateGalleryNDJSON:', error); console.error('[NDJSON Duplication] Error message:', error.message); console.error('[NDJSON Duplication] Error stack:', error.stack); return false; } } /** * Creates and shows a loading overlay for gallery duplication * @returns {Object} Object with hide function to remove the overlay */ function showDuplicationOverlay() { // Remove any existing overlay first const existingOverlay = document.getElementById('duplication-overlay'); if (existingOverlay) { existingOverlay.remove(); } // Add styles if not already present if (!document.querySelector('#duplication-overlay-styles')) { const style = document.createElement('style'); style.id = 'duplication-overlay-styles'; style.textContent = ` #duplication-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); z-index: 99999999; display: flex; align-items: center; justify-content: center; cursor: not-allowed; pointer-events: all; } #duplication-overlay .duplication-message { background: rgba(255, 255, 255, 0.95); padding: 30px 50px; font-family: Arial, sans-serif; font-size: 16px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); display: flex; flex-direction: column; align-items: center; gap: 20px; } #duplication-overlay .duplication-spinner { width: 80px; height: 80px; color: #444444; } `; document.head.appendChild(style); } const overlay = document.createElement('div'); overlay.id = 'duplication-overlay'; const message = document.createElement('div'); message.className = 'duplication-message'; const spinner = document.createElement('div'); spinner.className = 'duplication-spinner'; spinner.innerHTML = ` `; const text = document.createElement('div'); text.textContent = 'Duplicating gallery...'; text.style.textAlign = 'center'; message.appendChild(spinner); message.appendChild(text); overlay.appendChild(message); document.body.appendChild(overlay); return { hide: function() { if (overlay && overlay.parentNode) { overlay.parentNode.removeChild(overlay); } }, updateMessage: function(newMessage) { text.textContent = newMessage; } }; } /** * Duplicates a gallery or page item. * @param {number} id - The ID of the item to duplicate. * @param {Event} event - The click event. */ async function duplicateGalleryItem(id, event) { event.stopPropagation(); console.log(`Duplicating item with ID: ${id}`); // Show loading overlay const overlay = showDuplicationOverlay(); // Find the original item in the global galleries array const originalItem = findGalleryById(galleries, id); // Use helper if available, otherwise simple find if (!originalItem) { console.error(`Cannot duplicate: Item with ID ${id} not found.`); overlay.hide(); alert('Error: Could not find the item to duplicate.'); return; } try { // --- Create a Deep Copy --- // Using JSON.parse/stringify is a common way for simple object structures // Be cautious if your objects have Dates, functions, Maps, Sets, etc. const newItem = JSON.parse(JSON.stringify(originalItem)); // --- Modify Copied Item --- newItem.id = Date.now() + Math.floor(Math.random() * 1000); // Generate a new unique ID newItem.title = `${originalItem.title}-copy`; // Append "-copy" to title // Generate new pageId if it's a page or integrated gallery if (newItem.isPage || newItem.isIntegrated) { newItem.pageId = generatePageId(); // Assign a new unique pageId } // Generate new slug and URL const newBaseSlug = slugify(newItem.title); // Use the new title newItem.slug = ensureUniqueSlug(newBaseSlug, galleries); // Ensure uniqueness newItem.url = `/${newItem.slug}`; // Update URL based on new slug // Reset children (don't duplicate children for simplicity) newItem.children = []; // Deep clone galleryOptions and pageElements, update pageId if needed if (newItem.galleryOptions) { newItem.galleryOptions = JSON.parse(JSON.stringify(newItem.galleryOptions)); // Update pageId within options if it exists and matches the old one if (newItem.galleryOptions.pageId && newItem.galleryOptions.pageId === originalItem.pageId) { newItem.galleryOptions.pageId = newItem.pageId; } } if (newItem.pageElements) { newItem.pageElements = JSON.parse(JSON.stringify(newItem.pageElements)); // Give new IDs to duplicated page elements newItem.pageElements.forEach(el => { el.id = Date.now() + Math.floor(Math.random() * 10000); }); } // Reset home page status newItem.isHomePage = false; // --- Duplicate NDJSON if applicable --- // This must happen before saving the gallery console.error('[Gallery Duplication] Checking if NDJSON duplication is needed...'); console.error('[Gallery Duplication] newItem.pageId:', newItem.pageId); console.error('[Gallery Duplication] originalItem.pageId:', originalItem.pageId); if (newItem.pageId && originalItem.pageId) { overlay.updateMessage('Duplicating gallery files...'); console.error('[Gallery Duplication] Both galleries have pageIds, attempting to duplicate NDJSON file...'); const ndjsonDuplicated = await duplicateGalleryNDJSON(originalItem, newItem); if (!ndjsonDuplicated) { console.error('[Gallery Duplication] WARNING: NDJSON duplication failed, but continuing with gallery duplication'); console.error('[Gallery Duplication] User can republish the gallery later if needed'); // Don't block the duplication if NDJSON fails - user can republish later } else { console.error('[Gallery Duplication] SUCCESS: NDJSON duplication completed successfully'); } } else { console.error('[Gallery Duplication] SKIP: Skipping NDJSON duplication:', { hasNewPageId: !!newItem.pageId, hasOriginalPageId: !!originalItem.pageId, reason: !newItem.pageId ? 'New gallery has no pageId' : 'Original gallery has no pageId' }); } overlay.updateMessage('Saving duplicated gallery...'); // --- Insert into Galleries Array --- // Find the index of the original item const originalIndex = galleries.findIndex(item => item.id === id); if (originalIndex > -1) { // Insert the new item right after the original galleries.splice(originalIndex + 1, 0, newItem); console.log(`Inserted duplicate "${newItem.title}" after "${originalItem.title}"`); // Update positions for all items after the insertion point for (let i = originalIndex + 1; i < galleries.length; i++) { galleries[i].position = i; // Re-assign position based on new index } } else { // Fallback: Add to the end if original couldn't be found (shouldn't happen) console.warn(`Original item ${id} not found in array, adding duplicate to the end.`); newItem.position = galleries.length; galleries.push(newItem); } console.log('Rendering galleries immediately after duplication...'); renderGalleries(); // Update active states if necessary (though unlikely needed here) updateActiveStates(); // --- Save and Refresh --- console.log('Saving duplicated item...'); saveGalleries(null, { addedNewItem: true }) // Pass flag to trigger refresh .then(success => { overlay.hide(); // Hide overlay before page refresh if (success) { console.log('Duplicate saved successfully. Page will refresh.'); // No need to call renderGalleries() here because saveGalleries will trigger a reload. } else { console.error('Failed to save the duplicated item.'); alert('Error: Could not save the duplicated item.'); // Optionally try to revert the galleries array change here if save fails } }) .catch(error => { overlay.hide(); // Hide overlay on error console.error('Error saving duplicated item:', error); alert('Error saving duplicated item.'); }); } catch (error) { overlay.hide(); // Hide overlay on error console.error('Error duplicating item:', error); alert('An error occurred while duplicating the item.'); } } /** * saveGalleries function that always saves FULL data * Ensures gallery settings are preserved in both localStorage and server * * @param {Object} customData - Optional custom data to save instead of galleries * @returns {Promise} - Success indicator */ async function saveGalleries(customData) { if (!isAuthenticated && localStorage.getItem('hydra_is_admin') !== 'true') { alert('You must be logged in as an admin to save changes'); return false; } // Prevent multiple simultaneous saves if (window._saveInProgress) { console.log('Save already in progress, skipping'); return false; } window._saveInProgress = true; try { // CRITICAL: Synchronize galleries before saving to ensure we use the latest data if (typeof synchronizeGalleries === 'function') { synchronizeGalleries(); console.log('Galleries synchronized before saving'); } // Try to get a valid token from multiple sources let token = null; // First try the global didToken (traditional approach) if (didToken) { console.log('Using global didToken for save operation'); token = didToken; } // Next try TokenManager if available else if (window.TokenManager && typeof window.TokenManager.getToken === 'function') { token = window.TokenManager.getToken(); // Remove Bearer prefix if present (we'll add it later) if (token && token.startsWith('Bearer ')) { token = token.substring(7); } console.log('Using TokenManager token for save operation, length:', token ? token.length : 0); } // Fallback to localStorage else if (localStorage.getItem('hydra_auth_token')) { token = localStorage.getItem('hydra_auth_token'); console.log('Using localStorage token for save operation, length:', token.length); } // Last resort: try to get a fresh token else if (magic && magic.user) { try { console.log('Attempting to get fresh token from Magic'); token = await magic.user.getIdToken(); console.log('Obtained fresh token from Magic:', token ? 'success' : 'failed'); } catch (e) { console.error('Error getting token from Magic:', e); } } if (!token) { throw new Error('Could not find a valid authentication token. Please log in again.'); } // IMPORTANT: Try to save to localStorage with isolated error handling // This ensures localStorage failures don't prevent server saves if (!customData) { // Only try localStorage for regular gallery saves try { // FIXED: Always save the FULL galleries data to localStorage localStorage.setItem('galleries', JSON.stringify(galleries)); console.log(`Saved ${galleries.length} galleries to localStorage (FULL format with complete settings)`); } catch (storageError) { // Now storage errors are isolated and non-fatal console.warn('LocalStorage quota exceeded, proceeding without local backup', storageError); } } else if (customData && customData.isImport) { // Skip localStorage for import operations - data too large console.log('Skipping localStorage for import operation - data too large for local storage'); } // IMPORTANT: Ensure all pages have proper URLs and slugs before saving galleries.forEach(gallery => { if (gallery.isPage === true) { const pageSlug = slugify(gallery.title); // Make sure slug and URL are set correctly if (!gallery.slug || gallery.slug === '') { gallery.slug = pageSlug; } // Make sure URL points to slug if (!gallery.url || gallery.url === '/' || !gallery.url.includes(gallery.slug)) { gallery.url = `/${pageSlug}`; } } }); // Set up the data to save - ensure we're using the complete data let siteData; console.log('saveGalleries: customData received:', JSON.stringify(customData)); console.log('saveGalleries: siteMetadata in customData:', customData?.siteMetadata); console.log('saveGalleries: TITLE in customData:', customData?.siteMetadata?.title); console.log('saveGalleries: GOOGLE ANALYTICS in customData:', customData?.siteMetadata?.googleAnalytics); if (customData) { // CRITICAL FIX: Ensure gallery settings are preserved when saving from sidebar // If customData contains galleries (which it should for sidebar-initiated saves), // merge them with any existing settings to ensure nothing is lost if (customData.galleries && Array.isArray(customData.galleries)) { // First, ensure we preserve all current gallery settings from the existing galleries const existingGalleries = (typeof galleries !== 'undefined' ? galleries : window.galleries) || []; // Create merged galleries array with preserved settings const mergedGalleries = customData.galleries.map(gallery => { const existingGallery = existingGalleries.find(g => g.id === gallery.id); // If this gallery exists in the current dataset, ensure all galleryOptions are preserved if (existingGallery && existingGallery.galleryOptions && gallery.galleryOptions) { // Calculate settings depth const existingOptionsDepth = Object.keys(existingGallery.galleryOptions).length; const serverOptionsDepth = Object.keys(gallery.galleryOptions).length; // If server has fewer settings, merge them with existing settings if (serverOptionsDepth < existingOptionsDepth && existingOptionsDepth > 5) { console.log(`Preserving rich settings for ${gallery.title} (${existingOptionsDepth} vs ${serverOptionsDepth})`); // Create a merged gallery with preserved settings return { ...gallery, galleryOptions: { ...existingGallery.galleryOptions, // Start with all existing settings ...gallery.galleryOptions // Override with any new server settings } }; } } // If no preservation needed, use server gallery as is return gallery; }); // Use the merged galleries in customData siteData = { ...customData, galleries: mergedGalleries }; console.log(`Settings-preservation merge completed: ${mergedGalleries.length} galleries`); console.log('saveGalleries: siteData after merge:', JSON.stringify(siteData)); console.log('saveGalleries: siteMetadata after merge:', siteData.siteMetadata); console.log('saveGalleries: TITLE after merge:', siteData.siteMetadata?.title); console.log('saveGalleries: GOOGLE ANALYTICS after merge:', siteData.siteMetadata?.googleAnalytics); } else { siteData = customData; } } else { siteData = { galleries: galleries }; if (window.siteMetadata) { siteData.siteMetadata = window.siteMetadata; } if (window.SidebarManager) { siteData.sidebarElements = window.SidebarManager.elements; } } // Get the save URL const saveUrl = typeof getApiUrl === 'function' ? getApiUrl('/api/save-config') : '/api/save-config'; // Get user email from various sources const userEmail = (window.userMetadata && window.userMetadata.email) || localStorage.getItem('hydra_auth_email') || ''; // Ensure token has proper format const formattedToken = token.startsWith('hydra:') ? token : (token.includes('.') ? token : `hydra:${token}`); console.log('Making server save request...'); console.log('saveGalleries: Final siteData being sent to server:', JSON.stringify(siteData)); console.log('saveGalleries: Final siteMetadata being sent:', siteData.siteMetadata); console.log('saveGalleries: FINAL TITLE being sent:', siteData.siteMetadata?.title); console.log('saveGalleries: FINAL GOOGLE ANALYTICS being sent:', siteData.siteMetadata?.googleAnalytics); // Make the request const response = await fetch(saveUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${formattedToken}`, 'X-User-Email': userEmail, 'X-API-Request': 'true' // Always include this header for API requests }, body: JSON.stringify(siteData) }); console.log('Save response status:', response.status); // Process the response const responseText = await response.text(); console.log('Server response text (first 500 chars):', responseText.substring(0, 500)); let responseData; try { responseData = JSON.parse(responseText); console.log('Server response parsed successfully:', responseData); if (responseData.debug) { console.log('Server debug info:', responseData.debug); console.log('Server debug - requestBodyKeys:', responseData.debug.requestBodyKeys); console.log('Server debug - hasSiteMetadata:', responseData.debug.hasSiteMetadata); console.log('Server debug - siteMetadata:', responseData.debug.siteMetadata); console.log('Server debug - finalSiteMetadata:', responseData.debug.finalSiteMetadata); console.log('Server debug - googleAnalytics:', responseData.debug.googleAnalytics); } } catch (jsonError) { if (response.status === 200 && responseText.trim().startsWith('')) { responseData = { success: true }; } else { throw new Error('Invalid response format: ' + responseText.substring(0, 100)); } } if (!response.ok && !responseData.success) { throw new Error(`Failed to save: ${response.status} - ${JSON.stringify(responseData)}`); } console.log('Configuration saved successfully'); // Store new token if provided if (responseData.hydraToken) { console.log('Received new hydra token from server'); // Update the global didToken didToken = responseData.hydraToken.startsWith('hydra:') ? responseData.hydraToken.substring(6) : responseData.hydraToken; // Update localStorage localStorage.setItem('hydra_auth_token', didToken); // Update TokenManager if available if (window.TokenManager && typeof window.TokenManager.storeToken === 'function') { window.TokenManager.storeToken(responseData.hydraToken, userEmail); } } // Notify sidebar manager document.dispatchEvent(new CustomEvent('sidebar-save-requested')); // Process server response data if needed if (responseData.galleries) { // If server returns updated galleries data, update the global galleries variable if (Array.isArray(responseData.galleries) && responseData.galleries.length > 0) { // CRITICAL FIX: Before replacing galleries with server response, // preserve any gallery settings that might be lost in the response const updatedGalleries = responseData.galleries.map(serverGallery => { // Find the matching gallery in our current data const existingGallery = galleries.find(g => g.id === serverGallery.id); // Check if we need to preserve gallery settings if (existingGallery && existingGallery.galleryOptions && serverGallery.galleryOptions) { // Calculate settings depth const existingOptionsDepth = Object.keys(existingGallery.galleryOptions).length; const serverOptionsDepth = Object.keys(serverGallery.galleryOptions).length; // If server has fewer settings, merge them with existing settings if (serverOptionsDepth < existingOptionsDepth && existingOptionsDepth > 5) { console.log(`Preserving rich settings for ${serverGallery.title} (${existingOptionsDepth} vs ${serverOptionsDepth})`); // Create a merged gallery with preserved settings return { ...serverGallery, galleryOptions: { ...existingGallery.galleryOptions, // Start with all existing settings ...serverGallery.galleryOptions // Override with any new server settings } }; } } // If no preservation needed, use server gallery as is return serverGallery; }); // Update the galleries variable with our preserved data galleries = updatedGalleries; console.log(`Updated galleries from server response: ${galleries.length} galleries`); // IMPORTANT: Synchronize again after receiving server response if (typeof synchronizeGalleries === 'function') { synchronizeGalleries(); console.log('Galleries re-synchronized after server response'); } // Update localStorage with full data try { localStorage.setItem('galleries', JSON.stringify(galleries)); console.log('Updated localStorage with server gallery data (FULL format)'); } catch (e) { console.warn('Could not update localStorage with server gallery data', e); } } } // CRITICAL FIX: Clear all sessionStorage caches after successful save // This ensures that when users open the site in a new tab, they get fresh data try { console.log('Clearing sessionStorage caches after successful save...'); const keysToRemove = []; for (let i = 0; i < sessionStorage.length; i++) { const key = sessionStorage.key(i); if (key && (key.includes('gallery_data_') || key.includes('neonGallery_'))) { keysToRemove.push(key); } } keysToRemove.forEach(key => { sessionStorage.removeItem(key); console.log('Cleared sessionStorage key: ' + key); }); console.log('Cleared ' + keysToRemove.length + ' sessionStorage cache entries'); } catch (cacheError) { console.warn('Error clearing sessionStorage cache:', cacheError); // Don't fail the save if cache clearing fails } window._saveInProgress = false; return true; } catch (error) { console.error('Error saving configuration:', error); alert('Failed to save changes: ' + error.message); window._saveInProgress = false; return false; } } // Helper function to get API URL for preview sites function getApiUrl(endpoint) { // Check if we're on a preview URL const isPreviewUrl = window.location.hostname === 'preview.neonsky.app'; if (isPreviewUrl) { // Extract the site GUID from the path (first segment after domain) const pathParts = window.location.pathname.split('/').filter(Boolean); if (pathParts.length > 0) { const siteGuid = pathParts[0]; // Include the site GUID in the API URL return `/${siteGuid}${endpoint}`; } } // Regular case - return the endpoint as is return endpoint; } function updateActiveStates() { // Get the most current active gallery ID, ensuring consistency between both variables const currentActiveId = window.activeGalleryId || activeGalleryId; // Ensure both variables are synchronized window.activeGalleryId = currentActiveId; activeGalleryId = currentActiveId; console.log("updateActiveStates: Using active gallery ID:", currentActiveId); // Special case for invisible home page - don't try to highlight it in the menu const activeGallery = galleries.find(g => g.id === currentActiveId); if (activeGallery && activeGallery.visible === false && activeGallery.isHomePage === true) { console.log("Active gallery is invisible home page - not highlighting in menu"); // We're not returning here, so it will still update the visibility classes // but won't try to find an element to mark as active } // Fix selector issue - look for both .tree and .sidebar document.querySelectorAll('.tree li, .sidebar li').forEach(li => { if (!li.dataset.id) return; // Skip items without data-id attribute const id = parseInt(li.dataset.id); if (isNaN(id)) return; // Skip if ID is not a number const gallery = galleries.find(g => g.id === id); // Update active state using the most current ID const isActive = id === currentActiveId; // Add or remove the active class if (isActive) { li.classList.add('active'); console.log(`Setting active class for element: ${id} (${gallery ? gallery.title : 'Unknown'})`); } else { li.classList.remove('active'); } // Update visibility class if gallery exists if (gallery) { li.classList.toggle('hidden-gallery', gallery.visible === false); // Update visibility toggle icon if it exists const toggleButton = li.querySelector('.visibility-toggle'); if (toggleButton) { toggleButton.classList.toggle('hidden', gallery.visible === false); } } }); // Verify that the active state was actually set for the current activeGalleryId const activeElements = document.querySelectorAll('.active'); if (activeElements.length === 0 && currentActiveId) { // Only log a warning if we're not dealing with an invisible home page const activeGallery = galleries.find(g => g.id === currentActiveId); if (!(activeGallery && activeGallery.visible === false && activeGallery.isHomePage === true)) { console.warn(`No active elements found after update for ID: ${currentActiveId}`); } } // Log active items for debugging console.log("Active gallery ID after update:", currentActiveId); activeElements.forEach(el => console.log("Active element:", el.textContent.trim()) ); } function debugGalleryStructure() { console.group('Gallery Structure Debug'); // Log the raw data structure console.log('Raw galleries array:', JSON.parse(JSON.stringify(galleries))); // Log the tree structure const treeStructure = createGalleryTree(galleries); // Helper to print the tree function printTree(items, level = 0) { const indent = ' '.repeat(level); items.forEach(item => { console.log(`${indent}${item.title} (ID: ${item.id}, Parent: ${item.parentId || 'ROOT'})`); if (item.children && item.children.length > 0) { printTree(item.children, level + 1); } }); } console.log('Tree structure:'); printTree(treeStructure); // Log the DOM structure console.log('DOM structure:'); const treeEl = document.getElementById('galleryTree'); console.log(treeEl); console.groupEnd(); } function createGalleryTree(galleries) { console.log("Creating gallery tree with preservation safeguards..."); // First, create a map of galleries by id for quick lookup const galleryMap = {}; const processedIds = new Set(); // First pass: Create clean copies without children galleries.forEach(gallery => { if (!gallery || !gallery.id) return; // Skip invalid galleries // Create a copy of the gallery object without the children field const cleanGallery = { ...gallery }; delete cleanGallery.children; // Initialize an empty children array cleanGallery.children = []; // Add to map - even if we've seen this ID before, keep the latest version if (processedIds.has(gallery.id)) { console.warn(`Multiple galleries with ID ${gallery.id} - using latest version`); } processedIds.add(gallery.id); galleryMap[gallery.id] = cleanGallery; }); // Second pass: Assign children to their parents const rootGalleries = []; // To detect already-processed children const childrenAssigned = new Set(); galleries.forEach(gallery => { if (!gallery || !gallery.id) return; // Skip invalid galleries const galleryId = gallery.id; // Skip if this gallery isn't in our map (could be invalid) if (!galleryMap[galleryId]) return; // Skip if we've already assigned this gallery as a child if (childrenAssigned.has(galleryId)) return; if (gallery.parentId && galleryMap[gallery.parentId]) { // This is a child gallery, but first check it doesn't create a circular reference // Find all parents in the chain up to the root let currentParent = galleryMap[gallery.parentId]; let safeToAdd = true; let chainIds = [galleryId, gallery.parentId]; while (currentParent.parentId) { // If we've seen this ID in the chain, it would create a circular reference if (chainIds.includes(currentParent.parentId)) { console.warn(`Would create circular reference for gallery ${gallery.title} - making it a root gallery`); safeToAdd = false; break; } chainIds.push(currentParent.parentId); currentParent = galleryMap[currentParent.parentId]; // If parent doesn't exist in the map, break the chain if (!currentParent) break; } if (safeToAdd) { // Safe to add as child galleryMap[gallery.parentId].children.push(galleryMap[galleryId]); childrenAssigned.add(galleryId); } else { // Not safe - make it a root gallery rootGalleries.push(galleryMap[galleryId]); } } else { // This is a root gallery (no parent or parent doesn't exist) if (!childrenAssigned.has(galleryId)) { rootGalleries.push(galleryMap[galleryId]); } } }); // Final verification const allGalleryIds = new Set(galleries.map(g => g.id)); const treeGalleryIds = new Set(); // Function to collect all IDs in the tree const collectIds = (items) => { items.forEach(item => { treeGalleryIds.add(item.id); if (item.children && item.children.length > 0) { collectIds(item.children); } }); }; collectIds(rootGalleries); // Check if any galleries are missing from the tree const missingIds = []; allGalleryIds.forEach(id => { if (!treeGalleryIds.has(id)) { missingIds.push(id); } }); if (missingIds.length > 0) { console.warn(`${missingIds.length} galleries are missing from the tree structure`); // Add the missing galleries as root items missingIds.forEach(id => { const gallery = galleryMap[id]; if (gallery && !childrenAssigned.has(id)) { console.log(`Adding missing gallery to root: ${gallery.title}`); rootGalleries.push(gallery); } }); } console.log(`Created tree with ${rootGalleries.length} root galleries`); return rootGalleries; } function alignFolderStates() { console.log("Aligning folder data states with collapsed visual appearance"); let changed = 0; galleries.forEach(gallery => { if ((gallery.isFolder === true || gallery.isSubmenu === true) && gallery.isCollapsed === false) { gallery.isCollapsed = true; changed++; } }); } function renderGalleryItem(gallery, level = 0, maxLevel = 10) { // Prevent infinite recursion by limiting depth if (level >= maxLevel) { console.warn(`Maximum nesting level reached for gallery: ${gallery.title}`); return ''; } // Add debug info to console console.log(`Rendering gallery item: ${gallery.title}, isEditing=${isEditing}, level=${level}`); // Skip hidden items in view mode (but not spacers) if (!isEditing && gallery.visible === false && !gallery.isSpacer) { return ''; } const isPage = gallery.isPage === true; const isFolder = gallery.isFolder === true || gallery.isSubmenu === true; // Support both folder and legacy submenu const isSpacer = gallery.isSpacer === true; const isExternal = gallery.isExternal === true; // Determine if item has children const hasChildren = isFolder || gallery.children?.length > 0; const hasChildrenClass = hasChildren ? 'has-children' : ''; // Standard collapsed state check const isCollapsed = gallery.isCollapsed === true; // Simple expanded class logic - if the folder is not collapsed, add 'expanded' class const isExpandedClass = isFolder && !isCollapsed ? 'expanded' : ''; // Add additional classes for special types const spacerClass = isSpacer ? 'spacer-item' : ''; const externalClass = isExternal ? 'external-link' : ''; const folderClass = isFolder ? 'folder-item' : ''; const duplicateIconSvg = ` `; // NEW: Add nesting level as a data attribute for styling const nestingLevelAttr = `data-nesting-level="${level}"`; // Create the gallery item HTML with enhanced nesting indicators const html = `
    1. ${gallery.children && gallery.children.length > 0 ? gallery.children.map(child => renderGalleryItem(child, level + 1, maxLevel)).join('') : ''}
  • `; return html; } // Additional helper function to add visual indicators during drag operations function addDragVisualFeedback() { // Add event listener for drag operations document.addEventListener('dragover', function(e) { // Find the closest nested sortable list const nearestList = e.target.closest('.nested-sortable'); if (!nearestList) return; // Remove active-drop-target class from all lists document.querySelectorAll('.active-drop-target').forEach(el => { el.classList.remove('active-drop-target'); }); // Add it to the current list nearestList.classList.add('active-drop-target'); // Calculate if this would be a nested position // This is a simplified calculation - SortableJS has its own logic, // but this helps provide additional visual feedback const rect = nearestList.getBoundingClientRect(); const distanceFromLeft = e.clientX - rect.left; // If we're close to the left edge of a nested list, add a class if (distanceFromLeft < 30) { nearestList.classList.add('potential-parent'); } else { nearestList.classList.remove('potential-parent'); } }); // Remove classes when drag ends document.addEventListener('dragend', function() { document.querySelectorAll('.active-drop-target, .potential-parent').forEach(el => { el.classList.remove('active-drop-target', 'potential-parent'); }); }); } // Make sure the addDragVisualFeedback function is called when the document is ready document.addEventListener('DOMContentLoaded', function() { // Initialize the visual feedback helper addDragVisualFeedback(); // If we're in edit mode, make sure the nested sortables are initialized if (document.body.classList.contains('edit-mode-active')) { setTimeout(function() { initializeNestedSortables(); }, 300); } }); // Update the toggleEditMode function to ensure proper initialization of sortables const originalToggleEditMode = window.toggleEditMode; window.toggleEditMode = function() { originalToggleEditMode.apply(this, arguments); // Call original function stopAutoAdvanceTimer(); const sidebar = document.querySelector('.sidebar'); const editControls = document.querySelector('.edit-controls'); if (currentMenuLayout === 'horizontal') { if (document.body.classList.contains('edit-mode-active')) { // In horizontal layout and edit mode is ON: show sidebar and edit controls if(sidebar) sidebar.style.display = 'block'; // Or 'flex' if it's a flex container if(editControls) editControls.style.display = 'flex'; // Re-render the sidebar menu for editing renderGalleries(); } else { // In horizontal layout and edit mode is OFF: hide sidebar if(sidebar) sidebar.style.display = 'none'; // Re-render the horizontal menu for viewing renderHorizontalMenu(); } } else { // For other layouts (sidebar, top), ensure sidebar is visible if(sidebar) sidebar.style.display = 'block'; // Or 'flex' } }; function verifyGalleryStructure() { console.log("Verifying gallery structure integrity..."); // Count number of galleries const galleryCount = galleries.length; console.log(`Total galleries in flat array: ${galleryCount}`); // Create tree and count galleries in tree const tree = createGalleryTree([...galleries]); let treeCount = 0; function countGalleriesInTree(items) { items.forEach(item => { treeCount++; if (item.children && item.children.length > 0) { countGalleriesInTree(item.children); } }); } countGalleriesInTree(tree); console.log(`Total galleries in tree structure: ${treeCount}`); if (treeCount !== galleryCount) { console.warn(`GALLERY COUNT MISMATCH: ${galleryCount} in array vs ${treeCount} in tree`); return false; } else { console.log("Gallery structure is consistent"); return true; } } function renderGalleries() { console.log("Rendering galleries..."); // IMPORTANT: Synchronize with global edit state first const sidebar = document.querySelector('.sidebar'); if (sidebar && sidebar.classList.contains('editing')) { console.log("Edit mode detected from sidebar classes, ensuring isEditing is true"); isEditing = true; } try { // Only fix circular references if absolutely necessary const foundCircular = fixCircularReferences(); // Only create a new tree if we had to fix circular references let galleryTree; if (foundCircular) { console.log("Creating fresh gallery tree after fixing circular references"); galleryTree = createGalleryTree(galleries); } else { console.log("Using existing structure to create gallery tree"); galleryTree = createGalleryTree(galleries); } // Render the tree - IMPORTANT: Add null check const tree = document.getElementById('galleryTree'); if (!tree) { console.warn('galleryTree element not found, cannot render galleries'); return; // Exit the function if the element doesn't exist } // Render the tree with maximum depth protection tree.innerHTML = `
      ${galleryTree.map(gallery => renderGalleryItem(gallery, 0, 20)).join('')}
    `; // Initialize sortable for all nested sortable elements if (isEditing) { // Use requestAnimationFrame to ensure DOM is fully updated before initializing sortables // This prevents drag issues when items are added and immediately dragged requestAnimationFrame(() => { initializeNestedSortables(); }); } console.log("Galleries rendered successfully"); // Verify structure integrity after rendering verifyGalleryStructure(); } catch (error) { console.error("Error rendering galleries:", error); } } function fixCircularReferences() { console.log("Selectively checking for circular references..."); // Only process galleries that need checking: // 1. Imported galleries (they're new and might cause issues) // 2. Galleries with extremely deep nesting // 3. Actual circular references const processed = new Set(); // First pass: Identify actual circular references without modifying structure const circularIds = new Set(); const suspiciousIds = new Set(); galleries.forEach(gallery => { if (!gallery || !gallery.id) return; // Skip galleries we've already processed if (processed.has(gallery.id)) return; // Check for self-reference - this is always wrong if (gallery.parentId === gallery.id) { console.warn(`Self-reference detected: Gallery ${gallery.title} (${gallery.id}) is its own parent`); circularIds.add(gallery.id); return; } // Skip galleries without parents if (!gallery.parentId) { processed.add(gallery.id); return; } // Follow the parent chain to detect circular references const parentChain = [gallery.id]; let currentId = gallery.parentId; let loopCount = 0; let foundCircular = false; while (currentId && loopCount < 20) { // Reasonable limit to prevent infinite loops loopCount++; // If we've seen this ID before, it's a circular reference if (parentChain.includes(currentId)) { console.warn(`Circular reference detected in chain for gallery: ${gallery.title}`); circularIds.add(gallery.id); foundCircular = true; break; } parentChain.push(currentId); // Move up to the next parent const parent = galleries.find(g => g.id === currentId); currentId = parent?.parentId; // If we can't find the parent, we can stop if (!parent) break; } // If we hit our loop limit but didn't find a circular reference, // it's suspicious but not definitely circular if (loopCount >= 20 && !foundCircular) { console.warn(`Suspiciously deep parent chain for gallery ${gallery.title}, marking for review`); suspiciousIds.add(gallery.id); } // Mark this gallery and all its ancestors as processed parentChain.forEach(id => processed.add(id)); }); // Second pass: Only fix the identified circular references if (circularIds.size > 0) { console.log(`Fixing ${circularIds.size} confirmed circular references`); galleries.forEach(gallery => { if (circularIds.has(gallery.id)) { console.log(`Breaking circular reference for gallery: ${gallery.title}`); gallery.parentId = null; } }); } else { console.log("No circular references found"); } // For suspicious galleries, don't automatically fix them if (suspiciousIds.size > 0) { console.warn(`Found ${suspiciousIds.size} galleries with unusually deep nesting - not fixing automatically`); } return circularIds.size > 0; // Return true if we fixed anything } // Deep linking implementation that uses the existing slug property // Function to get the slug for a gallery function getGallerySlug(gallery) { // First try to use the existing slug property if (gallery.slug) { console.log(`Using existing gallery slug: ${gallery.slug}`); return gallery.slug; } // If no slug exists, generate one from the title as fallback if (gallery.title) { const generatedSlug = gallery.title .toLowerCase() .replace(/[^ws-]/g, '') .replace(/s+/g, '-') .replace(/--+/g, '-') .trim(); console.log(`Generated slug from title: ${generatedSlug}`); return generatedSlug || 'gallery'; } // Default fallback console.warn('Gallery has no slug or title, using default'); return 'gallery'; } // Function to update the URL when a gallery is loaded // Update the URL when a gallery is loaded function updateURLWithGallerySlug(gallery) { if (!gallery) return; // Get the slug for this gallery/page let slug = gallery.slug; if (!slug && gallery.title) { slug = gallery.title.toLowerCase().replace(/s+/g, '-').replace(/[^w-]+/g, '').replace(/--+/g, '-').trim(); } // Check if we're in preview mode (add this new code) const isPreviewMode = window.location.hostname === 'preview.neonsky.app'; // Check if current history state already has this galleryId const currentState = window.history.state; const currentPath = window.location.pathname; const isAlreadyCurrentPage = currentState && currentState.galleryId === gallery.id && ((isPreviewMode && currentPath.includes(slug)) || (!isPreviewMode && currentPath === '/' + slug)); if (isPreviewMode) { // Extract GUID from current URL path - first segment after domain const pathParts = window.location.pathname.split('/').filter(Boolean); const siteGuid = pathParts[0]; // Create history entry if we have a valid GUID and slug, and it's not already the current state if (siteGuid && slug) { const targetPath = '/' + siteGuid + '/' + slug; // Only push if URL is different OR if state doesn't match (allows multiple navigations) if (currentPath !== targetPath || !isAlreadyCurrentPage) { console.log('🔍 [updateURLWithGallerySlug] Updating URL to ' + targetPath + ' (preview mode), galleryId: ' + gallery.id); // Update browser URL without reloading, preserving the GUID window.history.pushState( { galleryId: gallery.id }, gallery.title || 'Gallery', targetPath ); // Update page title document.title = (gallery.title || 'Gallery') + ' - ' + window.location.hostname; } else { console.log('🔍 [updateURLWithGallerySlug] URL and state already match, skipping history update (preview mode)'); } } return; // Exit early, we've handled the preview case } // Regular (non-preview) URL handling if (slug) { const targetPath = '/' + slug; // Only push if URL is different OR if state doesn't match (allows multiple navigations) if (currentPath !== targetPath || !isAlreadyCurrentPage) { console.log('🔍 [updateURLWithGallerySlug] Updating URL to ' + targetPath + ', galleryId: ' + gallery.id); window.history.pushState( { galleryId: gallery.id }, gallery.title || 'Gallery', targetPath ); document.title = (gallery.title || 'Gallery') + ' - ' + window.location.hostname; } else { console.log('🔍 [updateURLWithGallerySlug] URL and state already match, skipping history update'); } } } function loadHomePage() { // Find a gallery marked as home page (regardless of visibility) const homePage = galleries.find(gallery => gallery.isHomePage === true); if (homePage) { console.log(`Loading home page: ${homePage.title} (Visible: ${homePage.visible !== false})`); // Update active gallery ID activeGalleryId = homePage.id; // Add a special flag to indicate we're loading the home page // This allows us to bypass visibility checks window._loadingHomePage = true; // Determine if this is a page or gallery and load appropriately if (homePage.isPage) { loadPage(homePage.id); } else { loadGallery(homePage.id); } // Clear the flag after loading setTimeout(() => { window._loadingHomePage = false; }, 100); // Update UI state updateActiveStates(); updateMobileTitle(); if (typeof closeMobileMenu === 'function') closeMobileMenu(); return true; } console.log('No home page defined'); return false; } // 4. Update the handleURLNavigation function to check for home page // Find the existing handleURLNavigation function and modify as follows: // In index.ts function handleURLNavigation() { const path = window.location.pathname.substring(1); // Remove leading slash const isPreviewMode = window.location.hostname === 'preview.neonsky.app'; let slug = path; let siteGuid = null; if (isPreviewMode && path) { const pathParts = path.split('/'); if (pathParts.length >= 1) { siteGuid = pathParts[0]; // Path could be just GUID/ or GUID/slug } if (pathParts.length >= 2) { slug = pathParts[1]; // Slug is the part after GUID console.log(`Preview URL detected, using slug: "${slug}" under GUID: "${siteGuid}"`); } else { // Only GUID is present, or path is empty after GUID slug = ''; // Treat as root/home for the given GUID console.log(`Preview URL with only GUID: "${siteGuid}", checking for home page or default.`); } } if (!slug) { // Handles root path ('/') or preview URL with only GUID ('/GUID/') console.log("No specific slug in path (root or GUID-only preview), checking for home page."); if (!loadHomePage() && galleries.length > 0) { const firstVisibleGallery = galleries.find(g => g.visible !== false && !g.isSpacer && !g.isFolder && !g.isSubmenu); if (firstVisibleGallery) { console.log("No home page, loading first visible item:", firstVisibleGallery.title); if (firstVisibleGallery.isPage) { loadPage(firstVisibleGallery.id); } else { loadGallery(firstVisibleGallery.id); } } else { console.log("No home page and no visible items to load for this path."); } } } else { console.log(`Handling URL navigation for slug: "${slug}"` + (isPreviewMode ? ` (Preview GUID: ${siteGuid})` : "")); // findGalleryByPath should be able to find the item using the slug, // handling preview mode internally if necessary or by being passed the correct slug. const matchingGallery = findGalleryByPath(slug, isPreviewMode, siteGuid); if (matchingGallery) { console.log('Loading content from URL: "' + matchingGallery.title + '" (ID: ' + matchingGallery.id + ')'); console.log('🔍 [handleURLNavigation] Gallery properties - isPage:', matchingGallery.isPage, 'pageId:', matchingGallery.pageId, 'has pageElements:', !!(matchingGallery.pageElements && Array.isArray(matchingGallery.pageElements))); if (matchingGallery.isSubmenu) { console.log('Gallery is a submenu, skipping direct load. Parent expansion might be needed.'); // Potentially, you might want to find and expand its parent here if the UI supports it. } else { // Set activeGalleryId *before* calling loadPage/loadGallery activeGalleryId = matchingGallery.id; window.activeGalleryId = matchingGallery.id; // Determine if this is a page by checking multiple indicators // Check isPage flag, pageId, or pageElements array const isPage = matchingGallery.isPage === true || (matchingGallery.pageId && matchingGallery.pageId.startsWith('page_')) || (matchingGallery.pageElements && Array.isArray(matchingGallery.pageElements) && matchingGallery.pageElements.length > 0); if (isPage) { console.log('🔍 [handleURLNavigation] Detected as PAGE, loading page: "' + matchingGallery.title + '"'); loadPage(matchingGallery.id); } else { console.log('🔍 [handleURLNavigation] Detected as GALLERY, loading gallery: "' + matchingGallery.title + '"'); loadGallery(matchingGallery.id); // This is async and should be awaited if subsequent logic depends on its completion. } } } else { console.log(`No matching gallery found for slug: "${slug}". Checking for home page as fallback.`); if (!loadHomePage()) { // Try loading home page const firstVisibleGallery = galleries.find(g => g.visible !== false && !g.isSpacer && !g.isFolder && !g.isSubmenu); if (firstVisibleGallery) { console.log(`Loading first visible gallery instead: "${firstVisibleGallery.title}"`); if (firstVisibleGallery.isPage) { loadPage(firstVisibleGallery.id); } else { loadGallery(firstVisibleGallery.id); } } else { console.log("No matching gallery, no home page, and no visible items to load."); // Optionally, display a 404 message in the gallery-container const galleryContainer = document.querySelector('.gallery-container'); if (galleryContainer) { galleryContainer.innerHTML = '
    Page not found.
    '; galleryContainer.style.opacity = '1'; } } } } } // Update UI states after initiating load. // These functions should ideally use the now-set activeGalleryId. if (typeof updateActiveStates === 'function') updateActiveStates(); // For sidebar/tree if (typeof updateMobileTitle === 'function') updateMobileTitle(); // For mobile header // **** ADDED FOR DEEP LINKING HORIZONTAL MENU **** // Determine current layout (it might not be set by MenuStyleCustomizer yet on direct load) let currentLayout = 'sidebar'; // Default assumption if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings && window.MenuStyleCustomizer.settings.menuLayout) { currentLayout = window.MenuStyleCustomizer.settings.menuLayout; } else { // Fallback: check body class if MenuStyleCustomizer hasn't initialized settings if (document.body.classList.contains('menu-layout-horizontal')) { currentLayout = 'horizontal'; } } console.log(`handleURLNavigation: Determined current layout as: ${currentLayout}`); if (currentLayout === 'horizontal') { if (typeof updateActiveStatesHorizontal === 'function') { console.log("handleURLNavigation: Explicitly calling updateActiveStatesHorizontal for horizontal layout on direct navigation."); // It's possible menu items aren't rendered yet by renderHorizontalMenu if this is a very early call. // A small delay might be needed, or ensure renderHorizontalMenu has run. // For now, call it directly. If items aren't there, it won't do anything harmful. setTimeout(() => { // Add a slight delay to allow menu rendering updateActiveStatesHorizontal(); }, 100); // Adjust delay if needed, or find a more robust way to ensure menu is rendered. } else { console.warn("handleURLNavigation: updateActiveStatesHorizontal function not found for horizontal layout."); } } } /** * Enhanced loadPage function that properly cleans up gallery content * @param {number} id - The page ID * @param {Event} event - Optional event object */ window.loadPage = function(id, event) { if (event) { event.preventDefault(); event.stopPropagation(); const now = Date.now(); const lastCallTime = window._lastPageLoadTime || 0; window._lastPageLoadTime = now; if (now - lastCallTime < 100) { console.log('Ignoring duplicate loadPage call'); return; } } console.error('🔍 [loadPage] Loading page with gallery ID:', id); stopAutoAdvanceTimer(); let galleriesData = window.galleries || galleries; const gallery = findGalleryById(galleriesData, id); if (!gallery) { console.warn('No gallery found with ID:', id, 'for page load.'); return; } if (typeof window.removeGalleryScriptsWithPause === 'function') { window.removeGalleryScriptsWithPause(); } window.activeGalleryId = id; activeGalleryId = id; const galleryContainer = document.querySelector('.gallery-container'); if (galleryContainer) { // Clear the container but ensure it's visible galleryContainer.innerHTML = ''; galleryContainer.style.display = 'block'; galleryContainer.style.visibility = 'visible'; galleryContainer.style.opacity = '1'; console.log('🔍 [loadPage] Cleared and ensured gallery-container is visible'); } else { console.error('🔍 [loadPage] gallery-container not found!'); } if (!gallery.pageId) gallery.pageId = `page_${id}`; if (!gallery.isPage) gallery.isPage = true; if (!window._pageIdToGalleryId) window._pageIdToGalleryId = {}; window._pageIdToGalleryId[gallery.pageId] = id; if (gallery.pageElements && Array.isArray(gallery.pageElements)) { if (window.PageManager && window.PageManager.elements) { window.PageManager.elements[gallery.pageId] = [...gallery.pageElements]; } } else if (window.PageManager && window.PageManager.elements && window.PageManager.elements[gallery.pageId] && window.PageManager.elements[gallery.pageId].length > 0) { gallery.pageElements = [...window.PageManager.elements[gallery.pageId]]; } if (window.PageManager && typeof window.PageManager.loadPage === 'function') { try { console.error('🔍 [loadPage] Calling PageManager.loadPage for pageId:', gallery.pageId); console.error('🔍 [loadPage] Gallery container exists:', !!galleryContainer, 'Container HTML length:', galleryContainer ? galleryContainer.innerHTML.length : 0); console.error('🔍 [loadPage] Page elements count:', gallery.pageElements ? gallery.pageElements.length : 0); console.error('🔍 [loadPage] PageManager.elements[' + gallery.pageId + '] exists:', !!(window.PageManager.elements && window.PageManager.elements[gallery.pageId])); // Small delay to ensure DOM is ready, especially when navigating back via history setTimeout(() => { // Verify container still exists const containerCheck = document.querySelector('.gallery-container'); console.error('🔍 [loadPage] Container check before loadPage:', !!containerCheck, 'Same as original:', containerCheck === galleryContainer); // PageManager.loadPage is async - we need to properly handle it try { const loadPromise = window.PageManager.loadPage(gallery.pageId); console.error('🔍 [loadPage] PageManager.loadPage called, returned:', typeof loadPromise, 'is promise:', loadPromise && typeof loadPromise.then === 'function'); if (loadPromise && typeof loadPromise.then === 'function') { loadPromise.then(() => { console.error('🔍 [loadPage] PageManager.loadPage promise resolved'); // Ensure container visibility after PageManager has initialized if (galleryContainer) { const pageContainer = galleryContainer.querySelector('.page-container'); if (pageContainer) { pageContainer.style.display = 'block'; pageContainer.style.visibility = 'visible'; pageContainer.style.opacity = '1'; const elementCount = pageContainer.querySelectorAll('.page-element').length; console.error('🔍 [loadPage] Page container found and made visible, has', elementCount, 'elements'); if (elementCount === 0) { console.error('🔍 [loadPage] WARNING: Page container exists but has no elements!'); console.error('🔍 [loadPage] PageManager.elements[' + gallery.pageId + ']:', window.PageManager.elements[gallery.pageId]); } } else { console.error('❌ [loadPage] Page container not found after PageManager.loadPage'); console.error('❌ [loadPage] Gallery container HTML:', galleryContainer.innerHTML.substring(0, 500)); console.error('❌ [loadPage] Gallery container children:', galleryContainer.children.length); } } else { console.error('❌ [loadPage] Gallery container is null after PageManager.loadPage'); } }).catch((error) => { console.error('❌ [loadPage] Error in PageManager.loadPage promise:', error); console.error('❌ [loadPage] Error stack:', error.stack); }); } else { // If loadPage doesn't return a promise, check after a delay console.error('🔍 [loadPage] loadPage did not return a promise, checking after delay'); setTimeout(() => { if (galleryContainer) { const pageContainer = galleryContainer.querySelector('.page-container'); if (pageContainer) { pageContainer.style.display = 'block'; pageContainer.style.visibility = 'visible'; pageContainer.style.opacity = '1'; console.error('🔍 [loadPage] Ensured page-container is visible (non-promise path)'); } else { console.error('❌ [loadPage] Page container not found (non-promise path)'); console.error('❌ [loadPage] Gallery container HTML:', galleryContainer.innerHTML.substring(0, 500)); } } }, 200); } } catch (loadError) { console.error('❌ [loadPage] Exception calling PageManager.loadPage:', loadError); console.error('❌ [loadPage] Error stack:', loadError.stack); } const isCurrentlyEditing = typeof isInEditMode === 'function' ? isInEditMode() : false; if (isCurrentlyEditing && typeof window.PageManager.setEditMode === 'function') { window.PageManager.setEditMode(isCurrentlyEditing); } }, 50); // Small delay to ensure DOM is ready } catch (error) { console.error('❌ [loadPage] Error in loadPage function:', error); console.error('❌ [loadPage] Error stack:', error.stack); } } else { console.error('❌ [loadPage] PageManager not found or loadPage method not available'); console.error('❌ [loadPage] window.PageManager:', !!window.PageManager); console.error('❌ [loadPage] typeof loadPage:', window.PageManager ? typeof window.PageManager.loadPage : 'N/A'); } // Apply menu visibility const bodyEl = document.body; if (gallery.hideMenuOnPage && !isInEditMode()) { // Check if not in edit mode bodyEl.classList.add('menu-hidden-on-page'); } else { bodyEl.classList.remove('menu-hidden-on-page'); // Ensure menu is visible if in edit mode or not hidden } if (typeof updateActiveStates === 'function') updateActiveStates(); if (typeof updateMobileTitle === 'function') updateMobileTitle(); if (typeof closeMobileMenu === 'function') closeMobileMenu(); // Close the gallery options panel when navigating to a different page if (typeof window.closeOptionsPanel === 'function') { window.closeOptionsPanel(); } if (typeof updateURLWithGallerySlug === 'function') updateURLWithGallerySlug(gallery); } /** * Ensures all required gallery styles are loaded */ function ensureGalleryStylesLoaded() { // Use Worker route for gallery CSS (avoids CDN SSL issues) const galleryScriptsBase = window.location.origin + '/gallery-scripts/'; const requiredStyles = [ { id: 'neon-gallery-css', href: galleryScriptsBase + 'neon-gallery-hp-hydra-v260118-001.css' }, { id: 'quill-css', href: 'https://cdn.quilljs.com/1.3.6/quill.snow.css' } ]; console.log('Loading gallery CSS from Worker:', galleryScriptsBase + 'neon-gallery-hp-hydra-v260118-001.css'); requiredStyles.forEach(style => { if (!document.getElementById(style.id)) { const link = document.createElement('link'); link.id = style.id; link.rel = 'stylesheet'; link.href = style.href; if (style.href.includes('cdn.neonsky.app')) { link.onerror = function() { console.error('❌ Gallery CSS failed, retrying via proxy:', link.href); // Set flag if not already set if (!window.useCdnProxy) { window.useCdnProxy = true; console.error('🚩 CDN_PROXY_FLAG SET: Gallery CSS failed, using proxy for all subsequent requests'); } const proxyUrl = link.href.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/'); link.href = proxyUrl; link.onerror = function() { console.error('❌ Proxy failed, trying storage:', proxyUrl); link.href = link.href.replace('cdn.neonsky.app', 'storage.neonsky.app'); }; }; } document.head.appendChild(link); console.log(`Added gallery style: ${style.id}`); } }); } /** * Ensures all required gallery scripts are loaded * @returns {Promise} A promise that resolves when all scripts are loaded */ function ensureGalleryScriptsLoaded() { const requiredScripts = [ { id: 'crypto-js', src: 'https://cdn.jsdelivr.net/npm/crypto-js@4.1.1/crypto-js.min.js' }, { id: 'quill-js', src: 'https://cdn.quilljs.com/1.3.6/quill.min.js' }, { id: 'quill-integration', src: 'https://cdn.neonsky.app/quill-integration-v260118-001.js' } ]; const promises = requiredScripts.map(script => { return new Promise((resolve, reject) => { // If script is already loaded, resolve immediately if (document.getElementById(script.id)) { resolve(); return; } // Create and load the script const scriptElement = document.createElement('script'); scriptElement.id = script.id; scriptElement.src = script.src; scriptElement.onload = resolve; scriptElement.onerror = () => reject(new Error(`Failed to load script: ${script.src}`)); document.head.appendChild(scriptElement); }); }); return Promise.all(promises); } /** * Creates a clean gallery context without using an iframe * @param {Object} gallery - The gallery object * @param {HTMLElement} container - The container to load the gallery into */ function createGalleryContext(gallery, container) { console.log('Creating gallery context for:', gallery.title); // CRITICAL: Ensure gallery CSS is loaded BEFORE loading the gallery script // This ensures styles are available even if NDJSON fails and falls back to classic JSON ensureGalleryStylesLoaded(); // Generate the page alias safely const pageAlias = gallery.slug || gallery.title.toLowerCase().replace(/s+/g, '-'); // Get our gallery options const galleryOptions = gallery.galleryOptions || {}; // Get token data let authToken = null; // First try TokenManager if available if (window.TokenManager && typeof window.TokenManager.getToken === 'function') { authToken = window.TokenManager.getToken(); } // Then try global token else if (window.didToken) { authToken = window.didToken; } // Set up Parameters for the gallery const galleryParameters = { SiteAlias: window.location.hostname, InitialPageUuid: gallery.pageId, InitialPageAlias: pageAlias, isInEditor: window.isEditing || false, siteId: window.siteId || '', isHydra: true }; // If we have a token, add it if (authToken) { galleryParameters.hydraAuthToken = authToken; } // Preserve original window.Parameters const originalParameters = window.Parameters; // REMOVED: Loading indicator - no longer showing the loading animation // Load the main gallery script - Use Worker route (avoids CDN SSL issues) const galleryScript = document.createElement('script'); const galleryScriptsBase = window.location.origin + '/gallery-scripts/'; galleryScript.src = galleryScriptsBase + 'neon-gallery-main-v260118-001.js'; console.log('Loading gallery-main script from Worker:', galleryScript.src); galleryScript.onerror = function() { console.error('❌ Gallery script failed, retrying via proxy:', galleryScript.src); const proxyUrl = galleryScript.src.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/'); galleryScript.src = proxyUrl; galleryScript.onerror = function() { console.error('❌ Proxy failed, trying storage:', proxyUrl); galleryScript.src = galleryScript.src.replace('cdn.neonsky.app', 'storage.neonsky.app'); }; }; // Setup the gallery configuration const neonGalleryConfig = { useData: true, useCDN: true, version: 'live', manualCollectionName: "mod", layoutType: "grid", ...galleryOptions, siteId: window.siteId || '' }; // Execute in a safe way that minimizes global namespace pollution try { // Set up the global variables needed by the gallery script window.Parameters = galleryParameters; window.neonGalleryConfig = neonGalleryConfig; // Monitor for script load/error galleryScript.onload = () => { console.log('Gallery script loaded successfully for:', gallery.title); // REMOVED: Don't need to remove the loading indicator since we don't add it }; galleryScript.onerror = () => { console.error('Failed to load gallery script for:', gallery.title); //container.innerHTML = ''; // Restore original parameters window.Parameters = originalParameters; }; // Add the script to load the gallery document.body.appendChild(galleryScript); // Set up a cleanup function that will be called when another gallery is loaded container.cleanup = () => { // Restore original parameters when cleaning up window.Parameters = originalParameters; // Remove the gallery script to prevent conflicts galleryScript.remove(); // Clear main gallery container if (container.parentNode) { container.innerHTML = ''; } }; } catch (error) { console.error('Error creating gallery context:', error); //container.innerHTML = ''; // Restore original parameters window.Parameters = originalParameters; } } /** * Only used for the PageManager's initializeDOM - ensures clean content swap */ function cleanGalleryContainer() { const galleryContainer = document.querySelector('.gallery-container'); if (galleryContainer) { galleryContainer.innerHTML = ''; } } /** * Initializes the page using the PageManager * To be used by PageManager.initializeDOM */ function initPageContent(pageId) { // First clear the container completely cleanGalleryContainer(); // Then load the page content if (window.PageManager && window.PageManager.loadPage) { window.PageManager.loadPage(pageId); } } /** * Cleanup gallery content when changing between galleries or pages */ function cleanupGalleryContent() { // Get the gallery container const galleryContainer = document.querySelector('.gallery-container'); if (!galleryContainer) return; // Find the direct gallery content const directContent = galleryContainer.querySelector('.gallery-direct-content'); if (directContent && typeof directContent.cleanup === 'function') { // Call the cleanup function directContent.cleanup(); } // Clear the container galleryContainer.innerHTML = ''; } /** * Simplified loadGallery function that follows the same content swap pattern * @param {number} id - The gallery ID * @param {Event} event - Optional event object */ // Override loadGallery to update URL window.loadGallery = async function(id, event) { // Stop event propagation if provided if (event) { event.stopPropagation(); } const galleryContainer = document.querySelector('.gallery-container'); if (galleryContainer) { // Apply immediate hiding styles galleryContainer.style.opacity = '0'; } console.log('Loading gallery with ID:', id); stopAutoAdvanceTimer(); // Find gallery by ID const gallery = findGalleryById(galleries, id); // Assuming `galleries` is accessible if (!gallery) { console.warn('No gallery found with ID:', id); if (galleryContainer) { galleryContainer.style.opacity = '1'; } // Show container if gallery not found return; } // Check if this is a page, if so use loadPage instead if (gallery.isPage === true) { // Use the loadPage function defined in this scope (or window.loadPage if you prefer) return loadPage(id, event); } // Skip submenu items if (gallery.isSubmenu) { console.log('Gallery is submenu, skipping URL update'); toggleSubmenu(id, event || { stopPropagation: () => {} }); // Assuming toggleSubmenu is available if (galleryContainer) { galleryContainer.style.opacity = '1'; } // Show container return; } console.log('Found gallery:', gallery.title); // CRITICAL: Set active gallery ID consistently in both global and window scope console.log('Setting active gallery ID to:', id, '(Previous value:', (window.activeGalleryId || activeGalleryId), ')'); window.activeGalleryId = id; activeGalleryId = id; // Ensure both variables are synchronized // Save reference to which gallery we're loading (for later verification) window.currentLoadingGalleryId = id; // CRITICAL: Thorough cleanup with await to ensure completion if (typeof window.removeGalleryScriptsWithPause === 'function') { await window.removeGalleryScriptsWithPause(); } // Clear all existing content from gallery container if (!galleryContainer) { console.error('Gallery container not found'); return; } // Force clear all content galleryContainer.innerHTML = ''; // CRITICAL: Double-check active gallery ID before updating UI if (window.activeGalleryId !== id || activeGalleryId !== id) { console.warn('Active gallery ID changed unexpectedly before UI update, restoring to:', id); window.activeGalleryId = id; activeGalleryId = id; } // Apply menu visibility based on the gallery's setting and edit mode const bodyEl = document.body; if (gallery.hideMenuOnPage && !isInEditMode()) { // Check if not in edit mode bodyEl.classList.add('menu-hidden-on-page'); } else { bodyEl.classList.remove('menu-hidden-on-page'); // Ensure menu is visible if in edit mode or not hidden } // Update UI state using explicit window function references or local fallbacks if (typeof window.updateActiveStates === 'function') { window.updateActiveStates(); } else if (typeof updateActiveStates === 'function') { updateActiveStates(); } if (typeof window.updateMobileTitle === 'function') { window.updateMobileTitle(); } else if (typeof updateMobileTitle === 'function') { updateMobileTitle(); } if (typeof window.closeMobileMenu === 'function') { window.closeMobileMenu(); } else if (typeof closeMobileMenu === 'function') { closeMobileMenu(); } // Update URL with gallery slug if (typeof window.updateURLWithGallerySlug === 'function') { window.updateURLWithGallerySlug(gallery); } else if (typeof updateURLWithGallerySlug === 'function') { updateURLWithGallerySlug(gallery); } // Create gallery container and load content const neonGalleryContainer = document.createElement('div'); neonGalleryContainer.id = 'neon-gallery-container'; neonGalleryContainer.className = 'gallery-direct-content'; neonGalleryContainer.setAttribute('data-gallery-id', gallery.id.toString()); neonGalleryContainer.style.width = '100%'; neonGalleryContainer.style.height = '100%'; galleryContainer.appendChild(neonGalleryContainer); // Add a cache-busting parameter to ensure script is freshly loaded const timestamp = Date.now(); // Set up gallery parameters window.Parameters = { SiteAlias: window.location.hostname, InitialPageUuid: gallery.pageId, InitialPageAlias: gallery.slug || slugify(gallery.title), // Assuming slugify is available isInEditor: window.isEditing || false, // Assuming window.isEditing is available siteId: window.siteId || gallery.siteId || '', // Assuming window.siteId is available isHydra: true, galleryInstanceId: timestamp, // Added loadedGalleryId as per your original function loadedGalleryId: gallery.id }; // Debug: Check what galleryOptions contains console.error('🔍 INDEX.TS DEBUG - Gallery:', gallery.title, 'has isPerma:', gallery.galleryOptions?.isPerma, 'permaURL:', gallery.galleryOptions?.permaURL, 'loadTxId:', gallery.galleryOptions?.loadTxId, 'loadPermaURL:', gallery.galleryOptions?.loadPermaURL); // Set up gallery config window.neonGalleryConfig = { useData: true, useCDN: true, version: 'live', manualCollectionName: gallery.galleryOptions?.manualCollectionName || "mod", layoutType: gallery.galleryOptions?.layoutType || "grid", // Spread gallery options AFTER setting defaults to ensure current gallery's settings take precedence // This ensures that only properties from the current gallery are included, not from previous galleries ...(gallery.galleryOptions || {}), // CRITICAL: Explicitly include Load Network fields to ensure they're passed to gallery script // These fields are required for the gallery to determine if it should use Load Network racing loadTxId: gallery.galleryOptions?.loadTxId || null, loadPermaURL: gallery.galleryOptions?.loadPermaURL || null, siteId: window.siteId || gallery.siteId || '', // Assuming window.siteId is available // Added galleryId and galleryInstanceId as per your original function - these OVERRIDE spread galleryId: gallery.id, galleryInstanceId: timestamp }; console.error('🔍 INDEX.TS DEBUG - After spread, window.neonGalleryConfig.isPerma:', window.neonGalleryConfig.isPerma, 'permaURL:', window.neonGalleryConfig.permaURL, 'loadTxId:', window.neonGalleryConfig.loadTxId, 'loadPermaURL:', window.neonGalleryConfig.loadPermaURL); // Create and add the gallery script with cache-busting // Use Worker route (avoids CDN SSL issues) const script = document.createElement('script'); const galleryScriptsBase = window.location.origin + '/gallery-scripts/'; script.src = galleryScriptsBase + 'neon-gallery-main-v260118-001.js'; // Using versioned file script.setAttribute('data-gallery-id', gallery.id.toString()); // Keep data-gallery-id attribute script.setAttribute('data-timestamp', timestamp.toString()); // Keep data-timestamp attribute console.log('Loading gallery-main script from Worker:', script.src); script.onerror = function() { console.error('❌ Gallery script failed, retrying via proxy:', script.src); const proxyUrl = script.src.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/'); script.src = proxyUrl; script.onerror = function() { console.error('❌ Proxy failed, trying storage:', proxyUrl); script.src = script.src.replace('cdn.neonsky.app', 'storage.neonsky.app'); }; }; // Added script.onload from your original function script.onload = function() { if (window.currentLoadingGalleryId !== gallery.id) { console.warn('Gallery ID mismatch! Expected:', gallery.id, 'Current:', window.currentLoadingGalleryId); } console.log(`Gallery script loaded for: ${gallery.title} (ID: ${gallery.id})`); }; if (galleryContainer) { setTimeout(() => { galleryContainer.style.opacity = '1'; }, 50); // Small delay to ensure content is ready } console.log(`Loading fresh gallery script for: ${gallery.title}`); document.body.appendChild(script); } // Function to ensure gallery container is visible function ensureGalleryVisibility() { const galleryContainer = document.querySelector('.gallery-container'); if (galleryContainer && galleryContainer.style.opacity === '0') { console.log('ensureGalleryVisibility: Setting gallery container opacity to 1'); galleryContainer.style.opacity = '1'; } } // Handle browser back/forward navigation window.addEventListener('popstate', async function(event) { // Make it async console.error('🔍 [Popstate] Event triggered. State:', event.state, 'Current URL:', window.location.pathname); let galleryIdToLoad = null; let galleryToLoad = null; // First, try to get galleryId from event.state if (event.state && event.state.galleryId) { galleryIdToLoad = event.state.galleryId; console.error('🔍 [Popstate] Found galleryId in event.state:', galleryIdToLoad); // Ensure galleries array is available and use findGalleryById helper const currentGalleries = window.galleries || galleries || []; galleryToLoad = findGalleryById(currentGalleries, galleryIdToLoad); } // If no gallery found from state, try to get it from the URL if (!galleryToLoad) { console.error('🔍 [Popstate] No galleryId in event.state or gallery not found, reading from URL:', window.location.pathname); if (typeof handleURLNavigation === 'function') { // handleURLNavigation will parse the URL and load the appropriate page/gallery console.error('🔍 [Popstate] Calling handleURLNavigation to parse URL and load content'); handleURLNavigation(); // This function should handle its own UI updates including active states. // After handleURLNavigation, update UI states setTimeout(() => { if (typeof updateActiveStates === 'function') updateActiveStates(); if (typeof updateMobileTitle === 'function') updateMobileTitle(); // Ensure gallery visibility after popstate navigation setTimeout(ensureGalleryVisibility, 1000); let currentLayout = 'sidebar'; if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings && window.MenuStyleCustomizer.settings.menuLayout) { currentLayout = window.MenuStyleCustomizer.settings.menuLayout; } else if (document.body.classList.contains('menu-layout-horizontal')) { currentLayout = 'horizontal'; } if (currentLayout === 'horizontal') { if (typeof updateActiveStatesHorizontal === 'function') { console.error("🔍 [Popstate] Calling updateActiveStatesHorizontal for horizontal layout with a delay."); setTimeout(() => { console.error("[Delayed Update from Popstate] Calling updateActiveStatesHorizontal."); updateActiveStatesHorizontal(); }, 150); } } }, 100); return; // Exit because handleURLNavigation will manage updates. } } // If we have a gallery from state, load it let contentLoaded = false; if (galleryToLoad) { console.error('🔍 [Popstate] Found gallery in history: "' + galleryToLoad.title + '" (ID: ' + galleryToLoad.id + ')'); console.error('🔍 [Popstate] Gallery properties - isPage:', galleryToLoad.isPage, 'pageId:', galleryToLoad.pageId, 'has pageElements:', !!(galleryToLoad.pageElements && Array.isArray(galleryToLoad.pageElements))); activeGalleryId = galleryToLoad.id; window.activeGalleryId = galleryToLoad.id; const editModeActive = typeof isEditing !== 'undefined' ? isEditing : (typeof window.isInEditMode === 'function' ? window.isInEditMode() : false); // When navigating back via browser history, we should load the content regardless of visibility // The URL itself indicates the user should see this content. Visibility checks are for menu display, not navigation. // Only skip if it's explicitly marked as not visible AND we're not in edit mode AND it's not a page const shouldLoad = galleryToLoad.visible !== false || editModeActive || galleryToLoad.isPage === true; console.error('🔍 [Popstate] Visibility check - visible:', galleryToLoad.visible, 'editModeActive:', editModeActive, 'isPage:', galleryToLoad.isPage, 'shouldLoad:', shouldLoad); if (shouldLoad) { // Determine if this is a page by checking multiple indicators // Check isPage flag, pageId, or pageElements array const isPage = galleryToLoad.isPage === true || (galleryToLoad.pageId && galleryToLoad.pageId.startsWith('page_')) || (galleryToLoad.pageElements && Array.isArray(galleryToLoad.pageElements) && galleryToLoad.pageElements.length > 0); if (isPage) { console.error('🔍 [Popstate] Detected as PAGE, loading page "' + galleryToLoad.title + '"'); if (typeof loadPage === 'function') { console.error('🔍 [Popstate] Calling loadPage for ID:', galleryToLoad.id); loadPage(galleryToLoad.id); contentLoaded = true; } else { console.error('❌ [Popstate] loadPage function not available'); } } else { console.error('🔍 [Popstate] Detected as GALLERY, loading gallery "' + galleryToLoad.title + '"'); if (typeof loadGallery === 'function') { await loadGallery(galleryToLoad.id); // Await here contentLoaded = true; } else { console.error('❌ [Popstate] loadGallery function not available'); } } } else { console.error('🔍 [Popstate] Gallery "' + galleryToLoad.title + '" is not visible and not in edit mode. Clearing content.'); const galleryContainer = document.querySelector('.gallery-container'); if (galleryContainer) galleryContainer.innerHTML = ''; // activeGalleryId = null; // Keep activeGalleryId to reflect URL, even if content not shown. } } else { console.error('❌ [Popstate] No gallery found and handleURLNavigation not available'); } // Update UI states after loading (or attempting to load) content if (typeof updateActiveStates === 'function') updateActiveStates(); if (typeof updateMobileTitle === 'function') updateMobileTitle(); // Ensure gallery visibility after popstate navigation setTimeout(ensureGalleryVisibility, 1000); let currentLayout = 'sidebar'; if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings && window.MenuStyleCustomizer.settings.menuLayout) { currentLayout = window.MenuStyleCustomizer.settings.menuLayout; } else if (document.body.classList.contains('menu-layout-horizontal')) { currentLayout = 'horizontal'; } if (currentLayout === 'horizontal') { if (typeof updateActiveStatesHorizontal === 'function') { console.log("Popstate: Calling updateActiveStatesHorizontal for horizontal layout with a delay."); setTimeout(() => { console.log("[Delayed Update from Popstate] Calling updateActiveStatesHorizontal."); updateActiveStatesHorizontal(); }, 150); // Slightly longer delay for popstate, as full content might be re-initializing } } }); // Check for direct URL access on page load document.addEventListener('DOMContentLoaded', function() { setTimeout(function() { console.log('Checking for deep links after DOM content loaded'); if (window.location.pathname !== '/') { handleURLNavigation(); } // Ensure gallery visibility after URL navigation setTimeout(ensureGalleryVisibility, 1000); }, 500); }); document.addEventListener('page-save-requested', function(e) { const data = e.detail; if (data && data.pageId && data.elements) { // Find the gallery/page that corresponds to this pageId const page = galleries.find(g => g.pageId === data.pageId); if (page) { // Store the page elements page.pageElements = data.elements; // Save to server saveGalleries(); } } }); document.addEventListener('DOMContentLoaded', function() { // Preload the Quill editor dependencies when page loads if (window.RichTextEditor && typeof window.RichTextEditor._ensureEditorDependencies === 'function') { console.log('Preloading Rich Text Editor dependencies'); window.RichTextEditor._ensureEditorDependencies().then(() => { console.log('Rich Text Editor dependencies preloaded successfully'); }).catch(error => { console.warn('Failed to preload Rich Text Editor:', error); }); } else { console.warn('Rich Text Editor not available for preloading'); // If not available yet, try again after a delay setTimeout(() => { if (window.RichTextEditor && typeof window.RichTextEditor._ensureEditorDependencies === 'function') { console.log('Retrying Rich Text Editor preload'); window.RichTextEditor._ensureEditorDependencies().catch(e => console.warn(e)); } }, 2000); } }); document.addEventListener('DOMContentLoaded', function() { const submenuCheck = document.getElementById('createSubmenu'); const submenuTitleField = document.getElementById('submenuTitle'); const submenuTitleGroup = submenuTitleField?.parentElement; if (submenuCheck && submenuTitleGroup) { submenuCheck.addEventListener('change', function() { submenuTitleGroup.style.display = this.checked ? 'block' : 'none'; }); } }); // Also check immediately if document is already loaded if (document.readyState !== 'loading') { setTimeout(function() { console.log('Document already loaded, checking for deep links'); if (window.location.pathname !== '/') { handleURLNavigation(); } }, 300); }